Post

I discovered SQL Injection via Code review

I discovered SQL Injection via Code review

1. Overview

I found the SQL Injection vulnerability in open source project in Gitee. I also try to contact with project owner but didn’t recieve any response :(((. And by the way, because I still not contact with owner so now I don’t public the domain or project. Some information about the vulnerability.

Vulnerability: SQL Injection

CVSS score: 9.8 (Critical)

Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Warning: This article is for research and educational purposes only. Attacking the system is prohibited and is against the law

2. Analysis

While review the source code, I found the interesting function. It’s a getListByPage().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public function getListByPage($map, $order = 'create_time desc', $field = '*', $r = 20)
    {
        if(empty($map)){
            $list  = $this->order($order)->field($field)->paginate(['list_rows' => $r, 'query' => request()->param()], false);
        }else{
            if (is_array($map)) {
                $list  = $this->where($map)->order($order)->field($field)->paginate(['list_rows' => $r, 'query' => request()->param()], false);
            } else {
                $list  = $this->whereRaw($map)->order($order)->field($field)->paginate(['list_rows' => $r, 'query' => request()->param()], false);
            }
        }
        
        return $list;
    }

Do you see that? It use whereRaw function, search for whereRaw we see that it allow user to inject the raw “where” clause into the query. img-description

Let deep dive into the logic of getListByPage(). First it will check is $map empty, if the $map is empty, it will use $order and $field. If $map is an array, it will use the Query Builder, and it prevent SQL Injection very well. And if $map is not an array, it will use the whereRaw - it let us to inject raw query. Coolllllll.

Now we know that the getListByPage() is the dangerous sink, we need to find out the source where let user enter data. After search in the project, I found that in the controller of index, a search controller let user search the for the keyword and it use getListByPage().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        $uid = get_uid();
        $keyword = trim(input('keyword', '', 'text')); // let user enter the input.
        View::assign('keyword', $keyword);
        // ...bla bla...
        $keyword = preg_replace('/\s+/u', ' ', $keyword); 
        $keyword_arr = preg_split('/\s+/', $keyword, 10, PREG_SPLIT_NO_EMPTY); 
        $keyword_quert_raw = '';
        foreach ($keyword_arr as $key => $val) {
            $keyword_quert_raw .= '`content` LIKE "%' . $val . '%"';
                if ($key < count($keyword_arr) - 1) {
                    $keyword_quert_raw .= ' OR ';
                }
            }
            
        $map = '`shopid` = ' . $this->shopid . ' AND (' . $keyword_quert_raw . ')';
        $fields = '*';
        $lists = (new SearchModel)->getListByPage($map, $order, $fields, $rows);
        //.... bla bla....

It look like the code is filter or santitize the $keyword but actully it just clean and split $keyword. So that mean we cannot using the space, because the keywork might split into each part. But we can use /**/ to replace the space. In mysql /**/ can use to represent to space, so we can use /**/ to bypass the string splitting.

Exploit:

The payload will be ")/**/AND/**/(SELECT/**/1/**/FROM/**/(SELECT(SLEEP(3)))a)/**/AND/**/("1. When we sent it, the server will sleep into 3 seconds. And you know what? It didn’t need to log in to use it, it make my SQL Injection found is critical.

I also test in their live production. Here is the POC. img-description Exploit with sleep 3 sec (may be deplay cause network) img-description Exploit with sleep 0 sec

3. Mitigate

To prevent SQL Injection, we can use filter or santitize the user input. Never trust user input.

In case we need to use whereRaw, we should bind the parameter to prevent SQLi.

Or force $map to an array like the code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        $uid = get_uid();
        $keyword = trim(input('keyword', '', 'text')); // let user enter the input.
        View::assign('keyword', $keyword);
        // ...bla bla...
        $keyword = preg_replace('/\s+/u', ' ', $keyword); 
        $keyword_arr = preg_split('/\s+/', $keyword, 10, PREG_SPLIT_NO_EMPTY); 
        $like_conditions = [];
        foreach ($keyword_arr as $val) {
            $like_conditions[] = ['content', 'LIKE', '%' . $val . '%'];
        }

        $map = [
            'shopid' => $this->shopid,
            '_complex' => [
                $like_conditions,
                '_logic' => 'OR'
                ]
            ];
        $fields = '*';
        $lists = (new SearchModel)->getListByPage($map, $order, $fields, $rows);
        //.... bla bla....
This post is licensed under CC BY 4.0 by the author.